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

# Buyer Onboarding

> End-to-end setup guide for agencies and brands buying media through Apostra API

This guide walks you end-to-end from "I have an API key" to "my first campaign is live." It is intended for agency teams, in-house brand marketers, and engineers building automated media-buying workflows on top of Apostra.

## Overview

By the end of this guide you will have:

* Confirmed your buyer identity, chosen a plan, and accepted the platform Terms of Service.
* Created an advertiser, linked to a brand identity.
* Configured measurement and (optionally) registered event sources for performance optimization.
* Run a discovery to find products that match your brief.
* Created, executed, and monitored your first campaign.

<Note>
  Prefer to drive everything through an AI agent? You can do every step below by talking to a connected MCP agent (Claude, ChatGPT, Cursor) — the agent will call these endpoints for you. See the [Quick Start](/v2/quickstart) for connector setup.
</Note>

***

## Request access before you have an account

<Note title="Buyer access is currently closed beta">
  Every buyer door — bringing your own agent or code over MCP and the API, and
  working through Claude, ChatGPT or our own chat and UI — is in closed beta. You
  ask to join the waitlist, we review the request, and you complete terms and
  payment setup once you are let in. The self-serve journey described below is
  how getting in works once the waitlist comes off, not what you meet today.
</Note>

Before account setup, Apostra may ask which Buyer or Seller product,
country, channel, and supply path you need. Product readiness, market
availability, and current supply are shown separately. If a product or market
cell is not ready, you can record an email notification for that exact cell.
If only Marketplace supply is missing, you can still continue, bring or connect
supply, browse another cell, or record the notification.

A waitlist submission records demand interest only. It does **not** create an
account, confirm ownership of the email domain, or grant product access. It also
does not put a future Account into a sandbox or pending lifecycle. When you
continue signup, email verification and Organization proof resolve or create
the Organization and Account; only ambiguous, conflicting, disputed, or risky
claims require manual review.

Choosing email notification records the exact disclosure text, locale, and
version you accepted. It remains pending and cannot send email until your
principal is verified. Before the intake is claimed, resubmitting the same
email updates the exact scoped interest. After claim, sign in to update
Organization interests; the public form will not modify reviewed evidence.

If your organization already has an Apostra account, an invitation or an
approved organization-access request remains the way you receive access. See
[Account Access & Signup](/v2/setup/account-access) for those paths.

***

## Prerequisites

### Pick how you'll authenticate

Pick **one** — you don't need both.

<CardGroup cols={2}>
  <Card title="OAuth (recommended for AI agents)" icon="user-shield">
    Connecting through Claude (Team/Enterprise/Desktop), ChatGPT MCP Connectors, or any OAuth-aware AI tool. Auth is handled automatically via the connector — no API key to manage. See [Built for Agents](/v2/setup/built-for-agents) for connector setup.
  </Card>

  <Card title="API key" icon="key">
    Required only for CLI tools, Cursor, custom MCP/HTTP clients, or programmatic backend integrations. Generate at [app.apostra.com/user-api-keys](https://app.apostra.com/user-api-keys); keys start with `scope3_`. See [Authentication](/v2/authentication) for full details.
  </Card>
</CardGroup>

The rest of this guide shows REST `curl` examples with `Authorization: Bearer $SCOPE3_API_KEY`. If you're using OAuth via Claude/ChatGPT, replace each step with the equivalent natural-language prompt — the connected agent will call the same endpoints for you.

### Optional

<Steps>
  <Step title="SSO">
    If your organization uses SAML SSO, ask your admin to configure it via the [SSO Setup guide](/v2/setup/sso-setup) before inviting team members.
  </Step>

  <Step title="Private storefront credentials">
    Some storefronts gate inventory behind per-source credentials (API key, OAuth, JWT). Register those at the storefront-source level — see the [Storefront object guide](/v2/object-guides/storefront#registering-credentials-per-source) for the walkthrough.
  </Step>
</Steps>

All REST examples in this guide use the buyer base URL:

```
https://api.apostra.com/api/v2/buyer
```

***

## End-to-End Flow

<Steps>
  <Step title="Step 1: Confirm your account and operator identity">
    Verify the API key resolves to the account you expect. The current account is the one that will own the advertisers and campaigns you create.

    ```bash theme={null}
    curl -X GET "https://api.apostra.com/api/v2/accounts/current" \
      -H "Authorization: Bearer $SCOPE3_API_KEY"
    ```

    Response:

    ```json theme={null}
    {
      "data": {
        "id": 1234,
        "company": "Acme Media",
        "name": "Acme Media",
        "role": "ADMIN"
      }
    }
    ```

    <Tip>
      If your team is structured as a parent organization with sub-brands or sub-agencies, list every account you can switch into with `GET /api/v2/accounts` and switch using `POST /api/v2/accounts/switch`. All subsequent API calls are scoped to the active account.
    </Tip>

    An account administrator must also confirm whether this buyer account represents
    the whole buying operator or a specific operating unit. A whole-operator account
    uses the operator domain alone. A specific unit adds a stable
    `operator_unit.id`, such as an established buying-seat, office, region, or team
    code. Review the other buyer accounts under the same operator before claiming
    the whole domain, and never use `default`, a temporary account name, a seller's
    account ID, or an Apostra database ID as the unit ID.

    Open Buyer Setup through your connected agent, or use the buyer readiness API,
    to review and confirm this identity. See [Buyer setup and go-live](/v2/buyer/account/setup)
    for the identity rules, existing-account migration behavior, and the distinction
    between account name, billing company, and stable unit ID.
  </Step>

  <Step title="Step 2: Choose a plan">
    Open Buyer Setup and continue to Plan & Billing to select the plan for this
    Buyer Account. Plan is an Account × product fact: it does not prove your
    Organization identity, accept Terms, provide payment authority, or guarantee
    that a particular market or supply source is available.
  </Step>

  <Step title="Step 3: Accept the Terms of Service">
    <Note>
      **Signed in via the web app?** If you registered or signed in at [app.apostra.com](https://app.apostra.com), you accepted ToS during that flow — skip this step and go straight to Step 3.
    </Note>

    If you signed up with an API key directly (not via the web app) and your billing
    organization does not yet have an active contract, real operations remain blocked
    until an organization administrator accepts the latest Platform Service Agreement:

    ```bash theme={null}
    curl -X POST "https://api.apostra.com/api/v2/accept-tos" \
      -H "Authorization: Bearer $SCOPE3_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "psaVersion": "v1.0"
      }'
    ```

    <Warning>
      Until ToS is accepted, the Account is limited to the documented demo and onboarding
      surfaces. Real operations return HTTP `403` with an actionable setup error such as
      `TOS_ACCEPTANCE_REQUIRED`.

      If you enter through a bring-your-own buyer agent registration, every allowed
      V3 MCP tool result appends this warning while the registration is still in its
      signup sandbox or its customer-bound Organization has not accepted current
      Terms: "Demo/onboarding environment: demo data is synthetic and has no
      commercial effect. Do not rely on demo results for commercial decisions or
      present them as market data. Do not enter confidential or personal
      information. Full notice:
      [https://apostra.com/agreements/demo-environment-notice](https://apostra.com/agreements/demo-environment-notice)". MCP hosts also
      receive the same notice in
      `_meta["scope3/demo-environment-warning"]`, with
      `environment: "demo_onboarding"`, `demoData: "synthetic"`, and
      `commercialEffect: false`, plus a machine-readable `noticeUrl`. Once current
      Terms are accepted, the warning stops and the Account's normal capability
      checks govern each operation.
    </Warning>

    <Note>
      **Discovering the current `psaVersion`**: use the `psaVersion` returned in the `TOS_ACCEPTANCE_REQUIRED` error response from any blocked write, or check the most recent agreement posted at [apostra.com](https://apostra.com/agreements/terms-of-service).
    </Note>
  </Step>

  <Step title="Step 4: Create an advertiser">
    An **advertiser** is the top-level container for campaigns. It carries brand identity, default optimization mode, and (optionally) UTM parameters and frequency caps that flow down to its campaigns.

    <CodeGroup>
      ```bash curl theme={null}
      curl -X POST "https://api.apostra.com/api/v2/buyer/advertisers" \
        -H "Authorization: Bearer $SCOPE3_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "name": "Acme Corp",
          "description": "Global advertising account for Acme Corporation",
          "brand": "acme.com",
          "optimizationApplyMode": "MANUAL",
          "sandbox": false
        }'
      ```

      ```typescript TypeScript theme={null}
      const res = await fetch(
        'https://api.apostra.com/api/v2/buyer/advertisers',
        {
          method: 'POST',
          headers: {
            Authorization: `Bearer ${process.env.SCOPE3_API_KEY}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            name: 'Acme Corp',
            brand: 'acme.com',
            optimizationApplyMode: 'MANUAL',
            sandbox: false,
          }),
        },
      )
      const { data: advertiser } = await res.json()
      ```
    </CodeGroup>

    Key fields:

    | Field                   | Description                                                                                                                    |
    | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
    | `name`                  | Display name for the advertiser (1–255 chars).                                                                                 |
    | `brand`                 | Domain (e.g. `acme.com`) or full manifest URL. Identity is resolved from `/.well-known/brand.json` or the AdCP brand registry. |
    | `optimizationApplyMode` | `AUTO` applies Apostra AI optimizations automatically; `MANUAL` requires approval. Defaults to `MANUAL`.                       |
    | `sandbox`               | When `true`, all downstream ADCP calls run against sandbox-flagged accounts — no real spend. **Immutable** after creation.     |
    | `linkedAccounts`        | Optional partner-platform accounts to link at creation (e.g. Snap, Google).                                                    |
    | `utmConfig`             | Click-URL parameters appended to landing-page URLs at delivery. None unless you set them.                                      |
    | `frequencyCaps`         | Buyer-side frequency caps Apostra enforces across publishers.                                                                  |

    <Tip>
      Building automation? Set `sandbox: true` for your first end-to-end run so nothing hits real exchanges or wallets.
    </Tip>
  </Step>

  <Step title="Step 5: Configure measurement (recommended)">
    Measurement configuration lives on the advertiser and is shared across all of its campaigns. You can opt into MMM, incrementality testing, brand lift, and other measurement programs.

    ```bash theme={null}
    curl -X PUT "https://api.apostra.com/api/v2/buyer/advertisers/12345/measurement-config" \
      -H "Authorization: Bearer $SCOPE3_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "mmmEnabled": true,
        "mmmConfig": {
          "provider": "measured",
          "reportingFrequency": "weekly"
        },
        "brandLiftEnabled": false
      }'
    ```

    To optimize campaigns against your own conversion events, register one or more **event sources** for the advertiser. This pushes ADCP-spec event source definitions into Apostra:

    ```bash theme={null}
    curl -X POST "https://api.apostra.com/api/v2/buyer/advertisers/12345/event-sources/sync" \
      -H "Authorization: Bearer $SCOPE3_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "account": { "account_id": "12345" },
        "event_sources": [
          {
            "event_source_id": "website_pixel",
            "name": "Acme Web Pixel",
            "event_types": ["page_view", "add_to_cart", "purchase"],
            "allowed_domains": ["acme.com", "shop.acme.com"]
          },
          {
            "event_source_id": "crm_uploads",
            "name": "Acme CRM Uploads",
            "event_types": ["purchase", "lead"]
          }
        ]
      }'
    ```

    <Note>
      Each event source must include a buyer-assigned `event_source_id` (1–255 chars). You'll reference it later when sending conversion events via the [Conversion API](/v2/integrations/conversion-api).
    </Note>

    Once registered, an event source can be referenced from a campaign's `performanceConfig.optimizationGoals` to drive bid optimization.

    <Note>
      Full conversion-tagging and measurement setup is covered in the [Measurement guide](/v2/guides/measurement-incrementality).
    </Note>
  </Step>

  <Step title="Step 6: Discover products">
    Discovery turns a natural-language brief and budget into a session of candidate products from every reachable sales agent.

    ```bash theme={null}
    curl -X POST "https://api.apostra.com/api/v2/buyer/discovery/discover-products" \
      -H "Authorization: Bearer $SCOPE3_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "advertiserId": "12345",
        "brief": "Premium CTV inventory targeting tech enthusiasts in the US",
        "budget": 50000,
        "flightDates": {
          "startDate": "2026-05-01T00:00:00Z",
          "endDate": "2026-05-31T23:59:59Z"
        },
        "channels": ["ctv", "video"],
        "countries": ["US"]
      }'
    ```

    The response contains a `discoveryId` (e.g. `disc_abc123`), grouped products (e.g. `prod_xyz789`), agent results, and (optionally) curated `proposals` you can apply directly to a campaign.

    <Tip>
      Discovery is iterative — call `POST /api/v2/buyer/discovery/:id/discover-products` again with `refine` to narrow results, or use `POST /api/v2/buyer/discovery/:id/apply-proposal` to load a curated set.
    </Tip>

    The full discovery workflow — refinement, proposals, manual product selection — is covered in the [Discovery guide](/v2/guides/discovery).
  </Step>

  <Step title="Step 7: Create your first campaign">
    A campaign references an advertiser and a (preselected) discovery session. The
    minimum required fields are `advertiserId`, `name`, `flightDates`, and `budget`.
    The legacy `routingType` field is not a campaign input; Apostra derives it
    per media buy as temporary billing compatibility metadata from `BillingParty`.
    It is never a storefront or execution type and does not determine campaign
    mode, BYOA, or protocol connectivity. See
    the [Campaign guide](/v2/object-guides/campaign) for details.

    ```bash theme={null}
    curl -X POST "https://api.apostra.com/api/v2/buyer/campaigns" \
      -H "Authorization: Bearer $SCOPE3_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "advertiserId": "12345",
        "name": "Acme Q2 CTV Launch",
        "flightDates": {
          "startDate": "2026-05-01T00:00:00Z",
          "endDate": "2026-05-31T23:59:59Z"
        },
        "budget": { "total": 50000, "currency": "USD" },
        "constraints": {
          "channels": ["ctv", "video"],
          "geo_countries": ["US"]
        },
        "discoveryId": "disc_abc123",
        "productIds": ["prod_xyz789"]
      }'
    ```

    Response:

    ```json theme={null}
    {
      "data": {
        "campaign": {
          "campaignId": "cmp_67890",
          "advertiserId": "12345",
          "name": "Acme Q2 CTV Launch",
          "status": "DRAFT"
        }
      }
    }
    ```

    Capture the `campaignId` from the response — we'll use this `cmp_*` id in Steps 7–8.

    Key choices:

    | Field                        | Notes                                                                                                                                                                                                                       |
    | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `discoveryId` + `productIds` | Attach a discovery session and pre-select products. Optional but typical.                                                                                                                                                   |
    | `performanceConfig`          | Add optimization goals tied to event sources or pacing metrics. Goals are hints — they don't lock composition. Set `autonomy.inventorySelection: "automatic"` separately to delegate composition to the platform optimizer. |
    | `pacingPeriods`              | Time-based pacing schedule with weight or fixed budget per period.                                                                                                                                                          |
    | `audienceConfig`             | Target / suppress audience IDs from the signals catalog.                                                                                                                                                                    |

    The campaign is created in `DRAFT` status — nothing has been sent to a sales agent yet.
  </Step>

  <Step title="Step 8: Add creatives">
    Before a campaign can launch, every product format selected on the campaign needs at least one creative that satisfies it. The creative endpoint is a single multipart request — a JSON `metadata` field plus one or more `files` parts:

    ```bash theme={null}
    curl -X POST "https://api.apostra.com/api/v2/buyer/campaigns/cmp_67890/creatives/create" \
      -H "Authorization: Bearer $SCOPE3_API_KEY" \
      -H "Content-Type: multipart/form-data" \
      -F 'metadata={"name":"Hero 30s CTV Spot","template_id":"video_standard","url_asset":{"url":"https://acme.com/promo","url_type":"CLICKTHROUGH"},"assets":[{"filename":"hero-30s.mp4","asset_type":"VIDEO","label":"main_video"}]};type=application/json' \
      -F "files=@hero-30s.mp4"
    ```

    Inspecting `GET /api/v2/buyer/campaigns/:id` returns a `creativeFormats` summary so you can see which formats are still missing creatives before launch. See the [Creatives guide](/v2/object-guides/creative) for the full lifecycle.
  </Step>

  <Step title="Step 9: Execute the campaign">
    Executing a campaign sends media-buy requests to every selected sales agent and transitions the campaign from `DRAFT` to `ACTIVE`.

    ```bash theme={null}
    curl -X POST "https://api.apostra.com/api/v2/buyer/campaigns/cmp_67890/execute" \
      -H "Authorization: Bearer $SCOPE3_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{}'
    ```

    Response:

    ```json theme={null}
    {
      "data": {
        "campaignId": "cmp_67890",
        "previousStatus": "DRAFT",
        "newStatus": "ACTIVE",
        "success": true
      }
    }
    ```

    If any media buys fail at the sales agent, `success` is `false` and `errors[]` lists the per-buy failures. After fixing the issue, you can re-run `POST /api/v2/buyer/campaigns/:id/execute` while the campaign is still `DRAFT`. The retry submits the same surviving DRAFT media buy; it does not rebuild the buy from discovery or discard per-buy creatives, flight dates, pacing, or optimization goals you changed after the failure.

    <Note>
      `execute` and `reactivate` are different endpoints. Use `POST /api/v2/buyer/campaigns/:id/execute` to launch a `DRAFT` (or `COMPLETED`) campaign. To resume a `PAUSED` campaign, use `POST /api/v2/buyer/campaigns/:id/reactivate` instead.
    </Note>

    <Tip>
      Need to pass `debug: true`? Add `{ "debug": true }` to the body — Apostra returns extra trace data from each upstream agent.
    </Tip>
  </Step>
</Steps>

***

## Monitoring

Once a campaign is `ACTIVE`, poll the live status of its media buys:

```bash theme={null}
curl -X GET "https://api.apostra.com/api/v2/buyer/campaigns/cmp_67890/media-buy-status" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

The response includes per-media-buy status pulled directly from each ADCP sales agent, plus a list of agents queried and any status changes detected on this poll. Pair this with the [Reporting overview](/v2/guides/reporting-overview) for delivery, spend, and performance metrics.

To pause a running campaign:

```bash theme={null}
curl -X POST "https://api.apostra.com/api/v2/buyer/campaigns/cmp_67890/pause" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

***

## Sandbox Testing

Set `sandbox: true` when creating an advertiser to run the entire flow above with no real spend. All downstream ADCP calls are tagged sandbox and routed to test accounts on the storefront's inventory sources. Sandbox mode is immutable after creation — you cannot promote a sandbox advertiser to production.

See [Sandbox mode](/v2/features/sandbox) for the full sandbox capability matrix.

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Advertiser object" href="/v2/object-guides/advertiser" icon="briefcase">
    Linked accounts, brand resolution, UTM templates, and frequency caps.
  </Card>

  <Card title="Campaign object" href="/v2/object-guides/campaign" icon="bullseye">
    Performance config, pacing, audience targeting, and execution lifecycle.
  </Card>

  <Card title="Discovery workflow" href="/v2/guides/discovery" icon="magnifying-glass">
    Briefs, refinement, proposals, and product selection.
  </Card>

  <Card title="Measurement workflow" href="/v2/guides/measurement-incrementality" icon="chart-line">
    Conversion events, MMM, incrementality, and brand lift.
  </Card>

  <Card title="Reporting" href="/v2/guides/reporting-overview" icon="chart-column">
    Delivery, pacing, spend, and performance reporting endpoints.
  </Card>

  <Card title="Storefronts" href="/v2/object-guides/storefront" icon="store">
    Browse storefronts and connect credentials per inventory source.
  </Card>
</CardGroup>
