# GET /v1/projects/:projectId/ads/ad-accounts (/docs/api/reference/ads/list-ad-accounts)



<Endpoint method="GET" path="/v1/projects/{projectId}/ads/ad-accounts" auth="Bearer" scope="ads:read" phase="1" />

Returns every ad account connected to the project across Meta, TikTok, and Apple Search Ads. Each row carries the platform-native id and current credential health. Layers' ad platform integrations are bring-your-own — partners connect their own ad accounts via OAuth and platform billing runs against the partner's own funding source. This is the first endpoint to hit when a partner agent needs to know which platforms a project can actually buy ads on right now.

<Parameters
  title="Path"
  rows="[
  { name: 'projectId', type: 'string (UUID)', required: true, description: 'Project to list within.' },
]"
/>

<Parameters
  title="Query"
  rows="[
  { name: 'platforms', type: 'string[]', description: 'Restrict to one or more platforms.', enum: ['meta_ads', 'tiktok_ads', 'apple_ads'] },
  { name: 'credentialMode', type: 'string', description: 'Filter by credential source. All Layers ad connections are `byo` today; `agency` is reserved.', enum: ['agency', 'byo'] },
  { name: 'healthy', type: 'boolean', description: 'Keep only ad accounts with tokenStatus=valid.' },
]"
/>

## Example request [#example-request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    curl "https://api.layers.com/v1/projects/prj_01HX9Y7K8M2P4RSTUV56789AB/ads/ad-accounts?healthy=true" \
      -H "Authorization: Bearer lp_live_01HX9Y6K7EJ4T2_4QZpN..."
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    const res = await fetch(
      `https://api.layers.com/v1/projects/${projectId}/ads/ad-accounts?healthy=true`,
      { headers: { Authorization: `Bearer ${apiKey}` } },
    );
    const { items } = await res.json();
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import httpx

    r = httpx.get(
        f"https://api.layers.com/v1/projects/{project_id}/ads/ad-accounts",
        params={"healthy": True},
        headers={"Authorization": f"Bearer {api_key}"},
    )
    items = r.json()["items"]
    ```
  </Tab>
</Tabs>

## Response [#response]

<Response status="200" description="OK">
  ```json
  {
    "items": [
      {
        "adAccountId": "01HXF1A2B3C4D5E6F7G8H9J0K1",
        "platform": "meta_ads",
        "externalId": "act_123456789012345",
        "name": "Acme Coffee — US",
        "currency": "USD",
        "timezone": "UTC",
        "credentialMode": "byo",
        "tokenStatus": "valid",
        "tokenLastValidatedAt": null,
        "walletBalance": null,
        "connectedAt": "2026-03-02T14:11:00Z"
      },
      {
        "adAccountId": "01HXF2A2B3C4D5E6F7G8H9J0K1",
        "platform": "tiktok_ads",
        "externalId": "7389201...",
        "name": "Acme Coffee TikTok",
        "currency": "USD",
        "timezone": "UTC",
        "credentialMode": "byo",
        "tokenStatus": "valid",
        "tokenLastValidatedAt": null,
        "walletBalance": null,
        "connectedAt": "2026-03-18T09:42:00Z"
      },
      {
        "adAccountId": "01HXF3A2B3C4D5E6F7G8H9J0K1",
        "platform": "apple_ads",
        "externalId": "org-9823412",
        "name": "Acme Coffee iOS",
        "currency": "USD",
        "timezone": "UTC",
        "credentialMode": "byo",
        "tokenStatus": "expired",
        "tokenLastValidatedAt": null,
        "walletBalance": null,
        "connectedAt": "2026-02-11T19:05:00Z"
      }
    ]
  }
  ```

  The response is unpaginated — each project has at most one credential row per platform, so `items` returns the union directly with no `nextCursor`.
</Response>

## Credential modes [#credential-modes]

* **`byo`** — The end-customer connected their own ad account via OAuth. This is the only mode shipping today. Billing runs against the customer's funding source on the platform; `walletBalance` is always `null` because Layers does not aggregate platform wallet state in Phase 1. Token refresh is automatic but tied to the customer's OAuth grant — a revoked grant surfaces as `tokenStatus: "expired"` until reconnected.
* **`agency`** — Reserved for a future agency / Business Center flow. No production projects use this mode yet.

## Token status [#token-status]

| Value     | Meaning                                                                                                                                          |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `valid`   | Last validation call succeeded. Safe to use.                                                                                                     |
| `expired` | Refresh attempt failed. Reconnect via [`POST /v1/projects/:projectId/ads/ad-accounts/oauth-url`](/docs/api/reference/ads/ad-accounts-oauth-url). |
| `revoked` | User revoked the grant on the platform. Reconnect required.                                                                                      |
| `pending` | Connection just initiated; first validation has not landed.                                                                                      |

`tokenLastValidatedAt` is reserved for the validation timestamp once Layers wires up periodic validation; it is `null` in Phase 1.

## Notes [#notes]

* `walletBalance` is reserved for a future agency / Business Center mode. It is always `null` today. For BYO, query the platform's own API if you need wallet state.
* `externalId` is the platform-native account id — `act_...` on Meta, a numeric string on TikTok, `org-...` on Apple. Use it when linking out to the platform's ad manager.
* `timezone` is reported as `"UTC"` for every account in Phase 1 — Layers does not yet pull the platform-native timezone. If you need the local-clock timezone the ads are bidding against, query the platform's own API.
* A `tokenStatus` of `expired` or `revoked` does not immediately pause running ads — the platform keeps those running from its side. New ad creation against that ad account will fail until reconnected.
* Disconnected ad accounts (rows where `disconnected_at` is set) are excluded from the response by default. There is no include-disconnected flag.

## See also [#see-also]

* [`POST /v1/projects/:projectId/ads/ad-accounts/oauth-url`](/docs/api/reference/ads/ad-accounts-oauth-url) — connect or reconnect
* [`GET /v1/projects/:projectId/ads/campaigns`](/docs/api/reference/ads/list-campaigns) — campaigns on these accounts
* [`GET /v1/projects/:projectId/ads/capi-status`](/docs/api/reference/ads/capi-status) — CAPI relay health per platform
