# Meta Marketing API: Managing Many Clients' Ad Accounts

How agencies and ad platforms run the Meta Marketing API across many client ad accounts: partner access, Facebook Login for Business, tiers, and rate limits.

Canonical page: https://www.tryadeli.com/blog/meta-marketing-api
Last updated: 2026-11-03

By Nick Reyes, Co-founder and CTO, Adeli. Published 2026-11-03.

## The short answer

The Meta Marketing API is Meta's API for creating, managing, and reporting on ads. To run it across many clients, an agency gets partner access to each client's ad account in Business Manager, while a platform has each customer connect through Facebook Login for Business. Both need Advanced Access to ads_management and Full Access in the Marketing API Access Tier. Adeli's Ads API gives platforms that access without running Meta's review.

The **Meta Marketing API** is Meta's API for creating, managing, and reporting on ads across Facebook, Instagram, and Meta's other apps. Calling it for one ad account takes an afternoon. Calling it for fifty client ad accounts is a different build: you need a way into accounts you don't own, permissions Meta only grants after review, and a scheduler that respects quotas counted **per ad account**.

This guide is for two readers. **Agencies** building internal tools to run many clients' ad accounts, and **ad and marketing platforms** putting Meta ads inside their own product for their customers. It covers the access model each one uses, the review and tiers both need, and how to keep rate limits and reporting under control as the account count grows. For single-topic depth, it links to the rest of our Meta series instead of repeating it.

## What does the Meta Marketing API do for a multi-account tool?

**The Meta Marketing API lets one app read and change ads in every ad account it has been granted, through the Graph API at `graph.facebook.com`.** Each ad account holds campaigns, each campaign holds ad sets, and each ad set holds ads. A multi-account tool runs the same calls across many accounts, so the hard parts are access, quotas, and reporting, not the objects.

The objects, in one line each:

| Object | ID format | Holds |
|---|---|---|
| **Ad account** | `act_<digits>` | Billing, currency, time zone, the client's spend |
| **Campaign** | digits | One objective |
| **Ad set** | digits | Budget, schedule, targeting |
| **Ad** | digits | The creative that delivers |

Our [Meta Ads API guide](https://www.tryadeli.com/blog/meta-ads-api) walks through the object model and a first call. This post starts where that one stops: at the second client.

## Which access model fits: partner access or customer login?

**Agencies usually reach client ad accounts through Business Manager partner access and a system user; platforms usually have each customer connect through Facebook Login for Business.** The right model depends on who owns the relationship with the ad account and whether clients self-serve.

| | **Agency internal tool** | **Platform for its customers** |
|---|---|---|
| Who uses the tool | Your own team | Your customers, inside your product |
| How you get into an ad account | Client shares it with your business portfolio as a partner | Customer clicks "Connect" and picks assets in Facebook Login for Business |
| Token | One system user token from your own portfolio | One token per customer: a user access token or a business integration system user token |
| Onboarding a new client | A request the client approves in Business Settings | Self-serve, in your app |
| Meta status you need | Advanced Access, business verification | Advanced Access, business verification, Tech Provider (for business integration tokens) |
| Breaks when | A client removes your partner access | A customer revokes the connection or loses their ad-account role |

Many teams end up with both: a partner-access setup for managed-service clients, and a login flow for self-serve ones. Store which model each ad account came through, because the token, the error handling, and the offboarding all differ.

## How do agencies get access to client ad accounts?

**An agency's business portfolio requests partner access to each client's ad account, the client approves it, and the agency assigns its system user to that account.** Meta's [Business-to-Business guide](https://developers.facebook.com/docs/marketing-api/businessmanager/business-to-business) puts it this way: "A Meta Business Manager may request access to an ad account or Page owned by another Business Manager."

The request goes to your own business's **`client_ad_accounts`** edge, with the tasks you need:

```bash
curl -X POST "https://graph.facebook.com/v26.0/$AGENCY_BUSINESS_ID/client_ad_accounts" \
  -F "adaccount_id=act_$CLIENT_AD_ACCOUNT_ID" \
  -F "permitted_tasks=['ADVERTISE','ANALYZE']" \
  -F "access_token=$AGENCY_ADMIN_TOKEN"
```

The request shows as `CLIENT_RESPONSE_PENDING` until an admin on the client side accepts it. Three details from Meta's docs trip people up:

- **The client has to expect you.** Meta says you can only send a request "to the Business Manager that you intend to approve and that they must already know your business." Agree on it on a call first.
- **The endpoints are gated.** The same page says "As of June 8, 2021, access to these endpoints is limited." If your app gets an error here, clients can still add your business as a partner by hand in Business Settings.
- **Don't claim ownership.** The `owned_ad_accounts` edge transfers an account into your portfolio. Meta's [ad accounts guide](https://developers.facebook.com/docs/marketing-api/business-manager/asset-management/ad-accounts) says "Most marketing companies won't need to claim ad accounts from their clients." Partner access keeps the client the owner, which is what they want when they leave.

Once the client approves, the account shows up under your business with `access_type` set to `AGENCY`. Assign your system user to it with the tasks it needs, then generate one system user token for the whole book of clients. Our [Meta system user access token guide](https://www.tryadeli.com/blog/meta-system-user-access-token) has the assignment and token calls step by step.

**One system user is a single point of failure.** Meta recommends a separate system user for each kind of access, so split reporting (`ANALYZE`) from campaign changes (`ADVERTISE`). On Limited Access you get one system user plus one admin system user; Full Access raises that to ten.

## How do platforms connect their customers' ad accounts?

**A platform sends each customer through Facebook Login for Business, which returns a token scoped to the business assets that customer selected.** Meta's [Facebook Login for Business docs](https://developers.facebook.com/docs/facebook-login/facebook-login-for-business/) call it "the preferred authentication and authorization solution for tech providers building integrations with Meta's business tools."

You set up a **configuration** in the App Dashboard (the permissions, the asset types, and the token type) and pass its `config_id` instead of `scope`. The token type is the main decision:

| | **User access token** | **Business integration system user access token** |
|---|---|---|
| Tied to | The person who logged in | The customer's business portfolio |
| Meta says use it when | "your app takes actions in real time, based on input from the user" | "your app performs programmatic, automated actions on your business clients' assets" |
| Lifetime | Short-lived, exchangeable for about 60 days | Defaults to never expire |
| Survives the person leaving | No | Yes |
| Available to | Any Business app | Tech Providers only |

For an ad platform that syncs metrics overnight and runs automations, the **business integration system user access token** is the better fit. It needs the authorization code grant, so the login call sets `response_type` and `override_default_response_type`:

```js
FB.login(
  (response) => sendCodeToServer(response.authResponse?.code),
  {
    config_id: META_CONFIG_ID,
    response_type: "code",
    override_default_response_type: true,
  },
);
```

Your server exchanges the code, then asks which business the token belongs to so you can file it against the right customer:

```bash
curl -G "https://graph.facebook.com/v26.0/oauth/access_token" \
  --data-urlencode "client_id=$APP_ID" \
  --data-urlencode "client_secret=$APP_SECRET" \
  --data-urlencode "code=$CODE"

curl -G "https://graph.facebook.com/v26.0/me" \
  --data-urlencode "fields=client_business_id" \
  --data-urlencode "access_token=$BUSINESS_INTEGRATION_TOKEN"
```

Meta's docs say your app "can only access the assets that were designated by your business client" during that login. If a customer adds a new ad account later, they go through the flow again to grant it. Our [Meta Ads API access token guide](https://www.tryadeli.com/blog/meta-ads-api-access-token) covers the plain user-token flow and how to debug a token that stops working.

## What access does a Meta app need to manage other businesses' ad accounts?

**An app that manages other businesses' ad accounts needs Advanced Access to `ads_management` (or `ads_read`), a verified business, and Full Access in the Marketing API Access Tier.** Meta's [authorization guide](https://developers.facebook.com/docs/marketing-api/get-started/authorization) draws the line plainly: "If your app is managing other people's ad accounts, you need advanced access."

Each gate is separate, and passing one doesn't pass the others:

| Gate | What it controls | How you get it |
|---|---|---|
| **Advanced Access** to `ads_management`, `ads_read`, `business_management` | Whether people outside your app's roles can grant the permission | App Review, per permission |
| **Business verification** | Whether other businesses can grant your app anything | Documents in Business Settings |
| **Marketing API Access Tier** | Rate limits and system user quota | Limited by default; Full after 500+ calls in 15 days with under 15% errors, then review |
| **Tech Provider** | Business integration system user tokens | Upgrade the app, or request the advanced permissions that make it one |
| **The person's role** | Which ad accounts a token can reach | The client grants it in Business Settings |

Meta renamed the tiers on May 4, 2026: Ads Management Standard Access became the Marketing API Access Tier, and its Standard and Advanced levels became **Limited Access** and **Full Access**. Meta describes Limited Access as "For development only. Not for production apps running for live advertisers." Guides that talk about a development or standard tier mean Limited Access. The tier is separate from Standard and Advanced Access to a permission, which kept their names.

Two notes for multi-tenant apps:

- **Request what your screencast shows.** A reviewer approves `ads_management` against a flow they can watch: a client connecting, picking an ad account, and a change made through your app. Our [Meta app review guide](https://www.tryadeli.com/blog/meta-app-review-time) covers what to record and how long a round takes.
- **Business verification comes first.** Until it clears, other businesses can't grant your app permissions at all. If yours is stuck, see [Meta business verification stuck in review](https://www.tryadeli.com/blog/meta-business-verification-stuck). Tech Provider Access Verification is gone, as our [Tech Provider guide](https://www.tryadeli.com/blog/tech-provider-access-verification-stuck) explains.

## How do rate limits work across many ad accounts?

**Meta counts most Marketing API quotas per ad account, per use case, over a rolling hour, so a multi-account tool needs a scheduler that tracks each account separately.** A busy client can be throttled while every other client's calls still go through.

On Limited Access, the `ads_management` quota is **300 calls an hour plus 40 per active ad**, per ad account. Full Access raises the base to 100,000. Reads and writes to one account also share a points score, and an ad set's budget can change only four times an hour. The full table of quotas and error codes is in our [Meta Marketing API rate limit guide](https://www.tryadeli.com/blog/meta-marketing-api-rate-limit).

The `X-Business-Use-Case-Usage` header reports usage on every response, keyed by business object ID. Read it on every call and park the account before it hits 100%:

```ts
type BucEntry = {
  type: string; // "ads_management", "ads_insights", ...
  call_count: number;
  total_cputime: number;
  total_time: number;
  estimated_time_to_regain_access: number; // minutes
};

const pausedUntil = new Map<string, number>(); // "<objectId>:<type>" -> epoch ms

export async function metaGet(path: string, token: string, params: Record<string, string> = {}) {
  const url = new URL(`https://graph.facebook.com/v26.0/${path}`);
  for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
  url.searchParams.set("access_token", token);

  const res = await fetch(url);
  const header = res.headers.get("x-business-use-case-usage");
  if (header) {
    const usage = JSON.parse(header) as Record<string, BucEntry[]>;
    for (const [objectId, entries] of Object.entries(usage)) {
      for (const e of entries) {
        const peak = Math.max(e.call_count, e.total_cputime, e.total_time);
        if (peak >= 75) {
          const waitMin = Math.max(e.estimated_time_to_regain_access, 5);
          pausedUntil.set(`${objectId}:${e.type}`, Date.now() + waitMin * 60_000);
        }
      }
    }
  }

  const body = await res.json();
  if (!res.ok) throw Object.assign(new Error(body.error?.message ?? res.statusText), { meta: body.error });
  return body;
}
```

Before each job, your queue checks `pausedUntil` for that account and use case and skips ahead to another client. The 75% threshold and five-minute floor are our defaults, not Meta's; tune them to your traffic.

## How do I pull insights for dozens of ad accounts?

**Start one asynchronous insights report per ad account, poll each report run, and read the results when it completes.** Synchronous `GET /insights` calls time out on long date ranges or ad-level breakdowns, and an async job can take up to an hour, so a nightly sync should never wait on accounts one at a time.

```ts
export async function startReport(adAccountId: string, token: string): Promise<string> {
  const res = await fetch(`https://graph.facebook.com/v26.0/${adAccountId}/insights`, {
    method: "POST",
    body: new URLSearchParams({
      level: "campaign",
      fields: "campaign_id,campaign_name,spend,impressions,clicks",
      date_preset: "last_30d",
      time_increment: "1",
      access_token: token,
    }),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(body.error?.message ?? res.statusText);
  return body.report_run_id;
}

export async function waitForReport(reportRunId: string, token: string): Promise<void> {
  for (;;) {
    const run = await metaGet(reportRunId, token, { fields: "async_status,async_percent_completion" });
    if (run.async_status === "Job Completed") return;
    if (run.async_status === "Job Failed" || run.async_status === "Job Skipped") {
      throw new Error(`${reportRunId}: ${run.async_status}`);
    }
    await new Promise((resolve) => setTimeout(resolve, 15_000));
  }
}
```

Read the rows from `GET /{report_run_id}/insights` and page with the cursors. In a multi-account sync:

- **Fan out a few accounts at a time.** Insights have their own quota per ad account and an app-wide limit, so cap concurrency and watch the `X-FB-Ads-Insights-Throttle` header.
- **Retry per account.** A failed or skipped job for one client shouldn't fail the whole night. Resubmit that account's report.
- **Don't keep report IDs.** A `report_run_id` expires after 30 days.

Fields, breakdowns, attribution windows, and retention are in our [Facebook Ads Insights API guide](https://www.tryadeli.com/blog/facebook-ads-insights-api).

## What breaks when a Meta Marketing API tool goes multi-tenant?

**Most multi-tenant failures come from access changing under you: a client revokes a partner, a person leaves, or a token loses a permission.** None of these show up while the only ad account is your own.

- **Testing with your own admin token.** Your token has a role on the app, so it works on Standard Access. A client's token won't until you have Advanced Access. Test with an account outside your app's roles before launch.
- **One token for everything.** A single user token from an employee breaks every client the day that person leaves. Use a system user token per job for partner access, and a token per customer for platform connections.
- **No record of how each account was connected.** Partner-access accounts fail with permission errors when the client removes your business; login-based accounts fail when the customer revokes the app. Store the access model per ad account so your support team knows who has to act.
- **Global retry loops.** Retrying a throttled call on a schedule shared by all clients turns one client's limit into everyone's delay. Back off per ad account.
- **Budget edits on every optimization pass.** An ad set's budget can change four times an hour, so collect the changes and send the final number once.
- **Creating campaigns live.** Create with `status=PAUSED` and activate in a separate step, so a bug never spends a client's money. Our [create-a-campaign guide](https://www.tryadeli.com/blog/facebook-ads-api-create-campaign) shows each object's create call.

New clients sometimes need a fresh ad account as well. Meta's limits on how many a new business can create are in our [create an ad account guide](https://www.tryadeli.com/blog/create-ad-account-meta-api).

## Should you build on the Meta Marketing API or use an API that already has access?

**Build directly when Meta ads are your core product and you can staff the review, the token storage, and the version upgrades; use an API like Adeli when Meta ads are one feature of a larger product.** Both paths call the same Meta endpoints underneath. The difference is who carries the Meta app.

| | **Build on the Meta Marketing API** | **Use Adeli's Ads API** |
|---|---|---|
| Meta app, App Review, business verification | Yours to pass and keep | Adeli's |
| Token storage and refresh per customer | You build it | Adeli holds each customer's connection |
| Rate-limit handling | Per-account scheduler you write | Throttling returns `429 rate_limited` with `Retry-After` |
| Version upgrades | Yours, on Meta's schedule | Adeli's |
| Control over every Marketing API field | Full | The operations Adeli's endpoints expose |
| Other networks | One integration per network | Organic posting, plus TikTok and LinkedIn Ads, through the same key |

**Adeli runs the Meta app and its review for you.** When we submitted `ads_management`, `pages_manage_ads`, and `leads_retrieval`, Meta's reviewer approved all three on the first submission (result received on October 15, 2026). We followed Meta's instructions for each permission as written, with a screencast of the full flow: login, consent, choosing the Page, creating a paused campaign, and the Leads tab.

Each of your customers connects a **Meta Ads account** to their Adeli profile, and [Adeli's Ads endpoints](https://www.tryadeli.com/docs/api/ads) then work across every ad account that connection reaches:

- **List businesses and ad accounts.** [List ad accounts](https://www.tryadeli.com/docs/api/ads/list-ad-accounts) returns accounts assigned directly and those owned by a business portfolio the customer administers.
- **Read campaigns and live metrics.** The campaign tree rolls up spend, impressions, reach, clicks, CTR, CPC, and CPM at every level, and daily insights come back per account or per object. Adeli reads these from Meta on each request instead of caching them, so a dashboard never shows yesterday's spend.
- **Create safely.** [Create campaign](https://www.tryadeli.com/docs/api/ads/create-campaign) promotes a post the Page already published, creates everything paused, and requires an `Idempotency-Key` so a retry never builds a second campaign.
- **Read leads.** Lead form submissions come back with the campaign, ad set, and ad each lead came from.

```bash
curl -G "https://app.tryadeli.com/api/v1/ads/accounts" \
  -H "Authorization: Bearer $ADELI_API_KEY" \
  --data-urlencode "accountId=$META_ADS_ACCOUNT_ID"
```

Meta's access tier and each customer's ad-account role still apply on top of Adeli's grants. If you're building an [AI marketing platform](https://www.tryadeli.com/use-cases/ai-marketing-platforms) or [vertical SaaS](https://www.tryadeli.com/use-cases/vertical-saas), each customer is one Adeli profile with its own connections, and [pricing](https://www.tryadeli.com/pricing) shows how those connections are billed.

## Frequently asked questions

### Does each client need their own Meta app?

No. One Meta app, owned by your business portfolio, serves every client. Each client grants that app access, either by sharing an ad account with your business as a partner or by connecting through Facebook Login for Business. Your app's App Review and access tier cover all of them.

### Can one system user manage every client's ad account?

For an agency, yes: assign the system user to each client ad account your business has partner access to. For a platform, each customer's Facebook Login for Business connection produces its own business integration system user token, scoped to the assets that customer chose, so tokens stay separate per customer.

### Do I need to be a Tech Provider to use the Marketing API?

Only to use business integration system user tokens from Facebook Login for Business, which Meta reserves for Tech Providers. An agency that works through partner access and its own system user doesn't need Tech Provider status, but still needs Advanced Access and business verification to touch clients' accounts.

## Related Adeli pages

- [Social Media and Ads API for AI Marketing Platforms](https://www.tryadeli.com/use-cases/ai-marketing-platforms.md): Run posting, analytics, inbox, and Meta ads for your customers' accounts through one API, without months of platform reviews.
- [Social Media API for Vertical SaaS Platforms](https://www.tryadeli.com/use-cases/vertical-saas.md): Give your small-business customers posting, scheduling, analytics, inbox, and Meta ads inside your vertical SaaS, through one API.
- [Adeli Pricing — priced per profile](https://www.tryadeli.com/pricing.md): Your first 2 profiles are free, with up to 2 social accounts each. Then $6 each for profiles 3–10, $4 for 11–50, $3 for 51–100, $2 for 101–500, and $1.50 from profile 501. Every platform is included in each profile.
- [All posts on the Adeli blog](https://www.tryadeli.com/blog.md)

## About the author

Nick Reyes is the CTO of Adeli. Previously he worked in security, cryptography, and authentication systems, with 10 years of experience at Google, Airbnb, and Phantom.
