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

# Automate Meta ad launches

> Turn creator content into Meta ads every day from your own code or from Claude: pick content, read your builder options, create and launch ads in one bulk call, then watch them publish.

<Info>
  **Prerequisite** A Growi organization on the Growth plan or higher, a
  connected Meta ad account with at least one campaign and ad set, and an
  owner or admin who can create an API key.
</Info>

# How to automate Meta ad launches

The API under `/api/public/v1/meta_ads`, and Claude with the Growi MCP
server, let you turn creator social posts, TikTok Shop videos and content
submissions into Meta ads in ad sets you already have. A daily job usually
looks like this:

1. [List ad sources](#1-list-ad-sources): new creator posts, TikTok Shop videos or approved submissions.
2. [Read the builder options](#2-read-the-builder-options): ad account, ad set, copy, CTA, UTMs and each creator's tracking link, in one call.
3. [Create the ads in bulk](#3-create-ads-in-bulk), up to 50 per request, launched as soon as they publish.
4. [Poll the ads](#4-poll-the-ads) while Growi publishes them to Meta.
5. [Launch or pause](#5-launch-or-pause) individual ads when you need to.

The seven endpoints are listed under [Meta ads](/api-reference/endpoint/meta-ads/list-ad-sources) in the API reference.

<Note>
  Campaigns, ad sets, targeting, ad account sync, partnership requests, Meta
  ad codes, copy templates, UTM params and naming are managed in the
  dashboard under **Meta Ads**. The API reads your saved settings and builds
  ads into the ad sets you set up there.
</Note>

## Before you start

### An API key

1. In the Growi dashboard, open **Settings → Developer**.
2. Under **API keys**, choose **Full access** in the access level menu.
3. Click **Add API key** and copy the key.

Only owners and admins can create keys. A key acts as the person who created
it, with their current role: if their role changes, so does what the key can
do, and if they leave the organization the key stops working.

* **Full access** keys can create, launch and pause ads, as long as the key's person is an owner or admin. If that person now has the viewer role, the key is read-only.
* **View only** keys can call the `GET` endpoints but get `403` on bulk, launch and pause.

Treat the key like a password: anyone who has it can spend on your Meta ad account.

### A connected Meta ad account with sync on

1. Connect Meta in **Settings → Integrations → Meta Ads**.
2. In **Meta Ads → Settings → Ad Accounts**, turn on auto-sync for every ad
   account you run creator ads from. The active account always syncs.
3. Create the campaigns and ad sets you want to fill in the **Meta Ads**
   builder, or import them from Ads Manager.

Ads publish with your organization's active Facebook page and Instagram
account, so both need to be connected. Spend and results only reach Growi,
and count toward creator payouts, for ad accounts that sync.

### Creators who are ready for partnership ads

To run an ad as the creator (a partnership ad, with the creator's name on it),
the creator must approve your partnership request on Instagram. Send requests
and check who approved in **Meta Ads → Partnerships**.
[List ad sources](/api-reference/endpoint/meta-ads/list-ad-sources) shows each
creator's readiness in `whitelisting_status` and gives you the approved
request in `meta_partnership_ad_request_id`.

Creators also need an active enrollment in the campaign that pays them for
Meta ads, so they have a Meta tracking link. See
[How creators get credited and paid](#how-creators-get-credited-and-paid).

## Authentication and base URL

```bash theme={"dark"}
Base URL: https://api.growi.io/api/public/v1
Authorization: Bearer YOUR_API_KEY
```

A parent organization's key can act on one of its brands by adding
`organization_id=<brand id>` to any request. Any other id returns `404`.

## 1. List ad sources

[List ad sources](/api-reference/endpoint/meta-ads/list-ad-sources) returns the
content you can build ads from. Pick a source:

| `source` | What it lists |
| - | - |
| `social_posts` (default) | Creator posts, newest first |
| `tik_tok_shop_videos` | TikTok Shop videos that sold in the date range, highest GMV first |
| `submissions` | Approved content submissions |

For a daily job, ask for content from the last day that is not in an ad yet:

```bash theme={"dark"}
curl -X GET "https://api.growi.io/api/public/v1/meta_ads/sources?source=social_posts&platform=instagram&ad_attachment=not_attached&start_date=2026-10-13&end_date=2026-10-14&per_page=100" \
     -H "Authorization: Bearer YOUR_API_KEY"
```

```json theme={"dark"}
{
  "success": true,
  "source": "social_posts",
  "data": [
    {
      "source": "social_post",
      "id": "instagram:3456789012345678901",
      "creative_source": { "social_post_id": "instagram:3456789012345678901" },
      "platform": "instagram",
      "creator": { "id": 2628998, "name": "Ava Creator", "handle": "ava.creates" },
      "campaign_id": 123,
      "campaign_affiliate_id": 3490313,
      "video_url": "https://cdn.growi.io/posts/3456789012345678901.mp4",
      "share_url": "https://www.instagram.com/reel/DAbCdEf/",
      "product": { "id": 88, "name": "Glow Serum" },
      "existing_meta_ad": null,
      "whitelisting_status": "whitelisted",
      "meta_partnership_ad_request_id": 4410
    }
  ],
  "meta": { "page": 1, "per_page": 100, "total_count": 1, "total_pages": 1, "has_more": false, "returned": 1 }
}
```

Other filters: `campaign_id`, `campaign_affiliate_id`, `creator_id` (Growi
user id), `search`, `page` and `per_page` (up to 100). Dates default to the
last 30 days and can span up to 366 days.

From each row you need:

* `creative_source`: copy its keys into the ad's `creative` as they are.
* `meta_partnership_ad_request_id`: send it to run the ad as the creator.
* `creator.id`: to get the creator's Meta tracking link in step 2.

## 2. Read the builder options

[Get builder options](/api-reference/endpoint/meta-ads/get-builder-options)
returns everything else you need in one call. Read it once at the start of a
run and reuse it for every ad.

```bash theme={"dark"}
curl -X GET "https://api.growi.io/api/public/v1/meta_ads/options?creator_ids=2628998,2629001" \
     -H "Authorization: Bearer YOUR_API_KEY"
```

| Field | Use it for |
| - | - |
| `ad_accounts[].id` | `meta_ad_account_id` (the Growi id, not the `act_` id) |
| `meta_ad_sets[]` | `meta_ad_set_id`. `ads_count` and `max_ads` show how much room each ad set has. |
| `meta_campaigns[]` | Which campaign each ad set belongs to |
| `pages[]`, `instagram_accounts[]` | Checking which identities the ads run with |
| `settings.naming` | Building each ad `name` |
| `settings.default_cta` | `creative.call_to_action` |
| `settings.copy_templates` | `creative.primary_text`, `headline` and `description` |
| `settings.utm_params` | `creative.url_tags` |
| `creator_links[].link` | `creative.link_url`, so the ad is credited to the creator |

Pages, campaigns and ad sets are for one ad account: the active one, or the
one you pass as `meta_ad_account_id`. Pass `meta_campaign_id` to see only the
ad sets of one campaign.

<Warning>
  The API does not apply your naming convention, default CTA, copy template
  text or UTM params for you. Read them here and put the values in each ad.
</Warning>

Meta allows 50 ads per ad set (150 with Advantage+ audience). If you build
more ads than one ad set can take, spread them across ad sets using
`max_ads - ads_count`, or add ad sets in the dashboard first.

## 3. Create ads in bulk

[Create ads in bulk](/api-reference/endpoint/meta-ads/bulk-create-ads) takes up
to 50 ads. Each row is checked and created on its own, so one bad row does not
stop the rest.

<Tip>
  Send each batch of up to 50 ads in **one** bulk call with
  `"launch_state": "active"`. The ads turn on by themselves as soon as they
  publish, so you do not need a launch call per ad. With the default limit of
  40 requests per hour, that keeps a daily run to a handful of requests.
</Tip>

```bash theme={"dark"}
curl -X POST "https://api.growi.io/api/public/v1/meta_ads/bulk" \
     -H "Authorization: Bearer YOUR_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "ads": [
         {
           "name": "Ava Creator | Glow Serum | 2026-10-14",
           "meta_ad_account_id": 1057,
           "meta_ad_set_id": 78552828,
           "launch_state": "active",
           "meta_partnership_ad_request_id": 4410,
           "copy_template_id": 7,
           "creative": {
             "social_post_id": "instagram:3456789012345678901",
             "primary_text": "The serum I reach for every morning.",
             "headline": "Glow in 7 days",
             "call_to_action": "SHOP_NOW",
             "link_url": "https://seranova.com/products/glow-serum?a=meta_ava25",
             "url_tags": "utm_source=meta&utm_medium=cpc&utm_content={{ad.id}}"
           }
         }
       ]
     }'
```

Each ad needs:

* `name`, `meta_ad_account_id` and `meta_ad_set_id` (an existing ad set of that ad account).
* `creative` with `primary_text`, `headline`, `call_to_action`, `link_url` and the row's `creative_source` keys.
* Optionally `launch_state` (`paused` by default, or `active`), `meta_partnership_ad_request_id` and `copy_template_id`.

The response is `207` with one result per row:

```json theme={"dark"}
{
  "success": true,
  "results": [
    { "index": 0, "success": true, "data": { "meta_ad_id": 5012, "state": "queued", "status": "PAUSED", "payload": { "meta_ad_id": 5012, "state": "queued" } } },
    {
      "index": 1,
      "success": false,
      "error": {
        "code": "PREFLIGHT_AD_SET_FULL",
        "message": "Ad set \"Broad US\" already holds 50 ads, Meta's maximum is 50. Pick another ad set or create a new one for the remaining ads.",
        "user_message": "Ad set \"Broad US\" already holds 50 ads, Meta's maximum is 50. Pick another ad set or create a new one for the remaining ads.",
        "details": { "ad_set_id": 78552828, "occupied": 50, "max": 50, "field": "ad_set.existing_meta_ad_set_id" }
      }
    }
  ],
  "summary": { "total": 2, "succeeded": 1, "failed": 1 }
}
```

Keep the `meta_ad_id` of every successful row: it is the Growi ad id you use
from now on.

## 4. Poll the ads

Growi publishes each ad to Meta in the background, usually within a few
minutes. Ask for all of a run's ads in one call with
[List ads](/api-reference/endpoint/meta-ads/list-ads), up to 200 ids:

```bash theme={"dark"}
curl -X GET "https://api.growi.io/api/public/v1/meta_ads?ids=5012,5013" \
     -H "Authorization: Bearer YOUR_API_KEY"
```

```json theme={"dark"}
{
  "success": true,
  "data": [
    { "id": 5012, "name": "Ava Creator | Glow Serum | 2026-10-14", "meta_ad_id": "120215550000000001", "state": "active", "status": "ACTIVE", "published": true, "failed": false, "error_message": null, "last_step": "launch", "meta_ad_set_id": 78552828, "meta_ad_set_name": "Broad US", "campaign_affiliate_id": 3490313, "external": false, "created_at": "2026-10-14T13:05:11Z" },
    { "id": 5013, "name": "Mia Creator | Glow Serum | 2026-10-14", "meta_ad_id": null, "state": "failed", "status": "PAUSED", "published": false, "failed": true, "error_message": "We could not fetch this post's video. Ask the creator for the file or use a content submission.", "last_step": "ensure_video", "meta_ad_set_id": 78552828, "meta_ad_set_name": "Broad US", "campaign_affiliate_id": 3490320, "external": false, "created_at": "2026-10-14T13:05:12Z" }
  ],
  "meta": { "page": 1, "per_page": 25, "total_count": 2, "total_pages": 1, "has_more": false },
  "by_state": { "active": 1, "failed": 1 }
}
```

| `state` | What to do |
| - | - |
| `queued`, `publishing` | Wait and poll again in a few minutes |
| `published` | On Meta and paused: launch it when you are ready |
| `active` | Live and spending |
| `failed` | Read `error_message` and `last_step`, fix the cause, and create the ad again (or retry it in the dashboard) |

`status` is Meta's own delivery status (`ACTIVE`, `PAUSED`, `DELETED`,
`ARCHIVED`). [Get an ad](/api-reference/endpoint/meta-ads/get-ad) returns the
same fields for one ad.

## 5. Launch or pause

```bash theme={"dark"}
curl -X POST "https://api.growi.io/api/public/v1/meta_ads/5012/launch" \
     -H "Authorization: Bearer YOUR_API_KEY"

curl -X POST "https://api.growi.io/api/public/v1/meta_ads/5012/pause" \
     -H "Authorization: Bearer YOUR_API_KEY"
```

* [Launch](/api-reference/endpoint/meta-ads/launch-ad) turns the ad, its ad set and its campaign on. Spend starts. An ad that has not published yet launches as soon as it does (`deferred: true`).
* [Pause](/api-reference/endpoint/meta-ads/pause-ad) pauses the ad on Meta.

Each call is one request, so use them for single ads. For a batch, set
`launch_state` on the bulk call instead.

## Example: a daily job

This Python sketch builds partnership ads from yesterday's Instagram posts
with your default copy template, fills the ad sets of one campaign, launches
them as they publish, and reports failures. It spaces requests 2 seconds
apart and waits out `429` responses.

```python theme={"dark"}
import time
from datetime import date, timedelta

import requests

API = "https://api.growi.io/api/public/v1"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
META_CAMPAIGN_ID = 902


def call(method, path, **kwargs):
    while True:
        response = requests.request(method, API + path, headers=HEADERS, timeout=120, **kwargs)
        if response.status_code == 429:
            time.sleep(int(response.headers.get("Retry-After", "2")) + 1)
            continue
        time.sleep(2)
        return response


today = date.today()
sources = call("GET", "/meta_ads/sources", params={
    "source": "social_posts", "platform": "instagram", "ad_attachment": "not_attached",
    "start_date": (today - timedelta(days=1)).isoformat(), "end_date": today.isoformat(), "per_page": 100,
}).json()["data"]
sources = [row for row in sources if row["meta_partnership_ad_request_id"]]

creator_ids = sorted({str(row["creator"]["id"]) for row in sources if row["creator"]["id"]})
options = call("GET", "/meta_ads/options", params={
    "meta_campaign_id": META_CAMPAIGN_ID, "creator_ids": ",".join(creator_ids),
}).json()["data"]
settings = options["settings"]
template = next(t for t in settings["copy_templates"] if t["default"])
cta = settings["default_cta"] or "SHOP_NOW"
url_tags = "&".join(f"{u['key']}={u['value']}" for u in settings["utm_params"])
links = {link["creator_id"]: link["link"] for link in options["creator_links"]}
room = [(s["id"], s["max_ads"] - s["ads_count"]) for s in options["meta_ad_sets"] if s["max_ads"] > s["ads_count"]]

ads = []
for row in sources:
    link = links.get(row["creator"]["id"])
    while room and room[0][1] == 0:
        room.pop(0)
    if not link or not room:
        continue
    ad_set_id, free = room[0]
    room[0] = (ad_set_id, free - 1)
    product = (row["product"] or {}).get("name", "Creator")
    ads.append({
        "name": f"{row['creator']['name']} | {product} | {today.isoformat()}",
        "meta_ad_account_id": options["meta_ad_account_id"],
        "meta_ad_set_id": ad_set_id,
        "launch_state": "active",
        "meta_partnership_ad_request_id": row["meta_partnership_ad_request_id"],
        "copy_template_id": template["id"],
        "creative": {
            **row["creative_source"],
            "primary_text": template["primary_text"],
            "headline": template["headline"],
            "call_to_action": cta,
            "link_url": link,
            "url_tags": url_tags,
        },
    })

created = []
for start in range(0, len(ads), 50):
    batch = ads[start:start + 50]
    result = call("POST", "/meta_ads/bulk", json={"ads": batch}).json()
    created += [r["data"]["meta_ad_id"] for r in result["results"] if r["success"]]
    for failure in (r for r in result["results"] if not r["success"]):
        print(batch[failure["index"]]["name"], failure["error"]["code"], failure["error"].get("message"))

pending = set(created)
while pending:
    time.sleep(300)
    rows = call("GET", "/meta_ads", params={"ids": ",".join(map(str, pending)), "per_page": 100}).json()["data"]
    for ad in rows:
        if ad["state"] in ("active", "failed"):
            pending.discard(ad["id"])
            if ad["failed"]:
                print(ad["id"], ad["last_step"], ad["error_message"])
```

This run makes 2 requests, plus one per batch of 50 ads and one per poll,
well inside the default hourly limit. Poll with `per_page` at or above the
number of ids you send.

## How creators get credited and paid

Growi credits each ad to one creator enrollment when you create it, and the
ad's `campaign_affiliate_id` shows which one:

* An ad built from a creator's **social post** is credited to the enrollment that post is attributed to.
* An ad built from a **content submission** is credited to the enrollment the submission came from.
* Otherwise, a `link_url` that carries the creator's **Meta tag** (`a=meta_<tag>`, from `creator_links` in [Get builder options](/api-reference/endpoint/meta-ads/get-builder-options)) credits the creator the tag belongs to.

Always use the creator's Meta link as `link_url`, whatever the source. Growi
attributes purchases to the creator through that tag. It also attributes ads
made outside Growi when their URL has `a=meta_<tag>`, `utm_source=meta` and
`utm_content={{ad.id}}`.

What the creator earns depends on the campaign they are enrolled in:

| Commission | How it is paid |
| - | - |
| Meta ad spend commission | A percentage of the spend on the ads credited to the creator's enrollment, each payout cycle. Spend comes from synced ad accounts only. A campaign can cap how much spend counts per ad over the ad's lifetime. |
| Meta creative commission, per sale | Commission on attributed purchases, through the creator's Meta tag |
| Meta creative commission, flat fee per ad | A flat fee recorded once, the first time a credited ad goes active |

A campaign can also require creators to deliver a number of posts before
their Meta commissions unlock.

Ad spend commission, sales and fees show up in the creator's payouts like any
other earnings. If a creator is removed from a campaign, ads already credited
to that enrollment stay on it: re-add the creator to the same campaign to keep
treating those ads as active on that program.

## Rate limits and batching

The Meta ads endpoints share your organization's public API limits:

* **2 seconds** between requests from the same key.
* **40 requests per hour** per key by default. Some organizations have a higher limit; the `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers on each response show yours.
* Over a limit you get `429` with a `Retry-After` header and `retry_after` in the body. The hourly limit resets at the top of the hour.

Claude and other MCP clients have their own budget and do not use these
requests. See [Connect Claude to Growi](/essentials/connect-claude#limits).

To stay inside the limit:

* Send up to 50 ads per [bulk request](/api-reference/endpoint/meta-ads/bulk-create-ads) with `launch_state: "active"`, instead of one request per ad and a launch call per ad.
* Read [builder options](/api-reference/endpoint/meta-ads/get-builder-options) once per run and reuse them.
* Poll [List ads](/api-reference/endpoint/meta-ads/list-ads) for up to 200 ids in one call, every few minutes.
* Use `per_page=100` on sources.

## Errors and troubleshooting

| Status | Meaning |
| - | - |
| `207` | Bulk create accepted. Check each row's `success`. |
| `401` | Missing or invalid key, or the key's creator is no longer an owner, admin or viewer of the organization |
| `403` | A view-only key on bulk, launch or pause, or a key whose person is not an owner or admin |
| `404` | `organization_id` is not one of your brands, the ad is not in your organization, or `meta_ad_account_id` on options is not one of your ad accounts |
| `422` | Invalid input: an empty or oversized batch, an unknown filter value, or Meta refused a launch or pause |
| `429` | Rate limited |

Rows in a bulk request fail with a `PREFLIGHT_*` code before anything is
created; the full list is on
[Create ads in bulk](/api-reference/endpoint/meta-ads/bulk-create-ads#row-error-codes).
Ads that fail while publishing end in `state: failed` with `error_message`
and `last_step`.

<AccordionGroup>
  <Accordion title="PREFLIGHT_MISSING_FIELD on meta_ad_set_id">
    `meta_ad_set_id` is missing, is not one of your ad sets, or belongs to a
    different ad account than `meta_ad_account_id`. Use an id from
    `meta_ad_sets` in builder options for the same ad account.
  </Accordion>

  <Accordion title="PREFLIGHT_AD_SET_FULL">
    The ad set already holds Meta's maximum of 50 ads (150 with Advantage+
    audience). Rows run in order and ads created by earlier rows in the same
    request count, so the rows past the limit fail. Spread rows across ad sets
    using `ads_count` and `max_ads` from builder options, or add an ad set in
    the dashboard.
  </Accordion>

  <Accordion title="PREFLIGHT_INVALID_CTA">
    `creative.call_to_action` must be a Meta CTA type such as `SHOP_NOW` or
    `LEARN_MORE`. `allowed.call_to_actions` in builder options lists every
    value.
  </Accordion>

  <Accordion title="PREFLIGHT_NO_INSTAGRAM_ACTOR">
    Your organization has no active Instagram account for the ad. Check
    `instagram_accounts` in builder options, or send
    `creative.require_instagram: false` to run the ad from the Facebook page
    only.
  </Accordion>

  <Accordion title="PREFLIGHT_NO_PIXEL">
    The ad set optimizes for conversions and the ad account has no Meta pixel.
    Install one, or pick an ad set with another goal.
  </Accordion>

  <Accordion title="PREFLIGHT_PARTNERSHIP_NOT_APPROVED">
    The partnership request is not approved yet, or Meta says the creator's
    Instagram account is not eligible. Check **Meta Ads → Partnerships**, or
    build the ad without `meta_partnership_ad_request_id` to run it from your
    page.
  </Accordion>

  <Accordion title="PREFLIGHT_CAMPAIGN_ARCHIVED">
    The ad set's campaign was archived or deleted in Ads Manager. Builder
    options hides closed campaigns and ad sets, so read it again and pick
    another ad set.
  </Accordion>

  <Accordion title="Sources return 422">
    A `campaign_id`, `campaign_affiliate_id` or `creator_id` that is not in
    your organization, a bad date or range, or `tik_tok_shop_videos` when no
    TikTok Shop is connected. The `error` field says which.
  </Accordion>

  <Accordion title="An ad failed with 'We could not fetch this post's video'">
    Growi could not download the post's video. Ask the creator for the file,
    or build the ad from a content submission instead.
  </Accordion>

  <Accordion title="The creator gets no spend commission for an ad">
    Check that the ad has the creator's `campaign_affiliate_id` (Get an ad),
    that its ad account syncs, and that the creator's campaign pays Meta ad
    spend commission. Use the creator's Meta link as `link_url` on new ads.
  </Accordion>
</AccordionGroup>

## Do the same with Claude

If you [connected Claude to Growi](/essentials/connect-claude), you can run the
whole flow by asking, for example: "Build partnership ads from yesterday's
Instagram posts into the Broad US ad set, launch them, then tell me which ones
failed."

| Tool | Step | What it does |
| - | - | - |
| `list_meta_ad_sources` | 1 | Lists social posts, TikTok Shop videos or submissions, with each row's `creative_source`, creator, readiness and partnership request (same as `GET /meta_ads/sources`) |
| `get_meta_ad_builder_options` | 2 | Ad accounts, pages, Instagram identities, open campaigns and ad sets (with `ads_count` and `max_ads`), naming, default CTA, copy templates, UTM params, saved links, allowed CTAs, and creators' Meta links (same as `GET /meta_ads/options`) |
| `create_meta_ads` | 3 | Creates up to 50 ads in existing ad sets with the same rows as the bulk endpoint |
| `list_meta_ads` | 4 | Publish state of ads built in Growi, newest first, filtered by id, state, Meta status or ad set |
| `manage_meta_ads` | 5 | Launches or pauses up to 50 ads |

`create_meta_ads` and `manage_meta_ads` need an owner or admin, connected with
their Growi login or with a full-access key they created. Both ask before they
change anything:

1. Claude calls the tool without confirming. Nothing is created or changed:
   `create_meta_ads` checks every ad and reports which ones would fail and
   which would start spend, and `manage_meta_ads` lists the ads that would
   change.
2. Claude shows you the plan.
3. After you approve, Claude calls the tool again with the same arguments and
   the confirmation token from step 1. The token works once, only for those
   exact arguments, and expires after 10 minutes.

On the MCP budget, `list_meta_ad_sources` costs 5 units, `create_meta_ads`
and `manage_meta_ads` cost 3, and the other tools cost 1.


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