> ## 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.

# Manage buyer agents

> Add connected software agents to your buyer organization and manage their access, connections, and credentials.

## What a buyer agent is

A **buyer agent** is a connected software actor installed in your buyer
organization. It is not a person, and no person is assigned to or represented
by an agent. Agents sit alongside Advertisers in the buyer workspace.

The product keeps six concepts separate:

* **Agent** — the software actor you recognize by name and purpose.
* **Installation** — that agent's presence in your organization.
* **Principal** — the durable internal identity Apostra uses for routing
  and audit. Most people only need it when troubleshooting.
* **Credential** — how the software proves its identity. Secrets are shown
  once and are never displayed again.
* **Connection** — a configured way for the agent to connect to Apostra,
  exchange data, or use an organization-owned reporting resource.
* **Grant** — explicit permission to act for an advertiser or account. An
  installation, principal, credential, or connection is not a grant.

Adding an agent does not grant advertiser, account, spend, payment, Terms, or
other business authority. Until an administrator grants an advertiser, the
agent shows **No advertisers granted yet**.

The Agents area is designed for two operation modes under that same model:

* **External agent** — software operated outside Apostra and connected
  through the supported principal, credential, and connection flows.
* **Apostra-hosted agent** — software whose runtime is provisioned and
  monitored by Apostra.

The supported Add agent flows currently connect external software. Hosted
creation is not shown until its production installation and provisioning API is
available; a local development runtime is not an organization registry.

## Before you start

Only a person who is signed in directly as an administrator of the
organization can read or change any of this. A support session, an impersonated
or simulated session, a delegated session, an API key, and an OAuth client are
all refused — including for reads. The organization is always resolved from who
you are signed in as; a request cannot name a different organization, and it
can never reach another organization's principals.

Use the shared V2 base URL:

```bash theme={null}
BASE=https://api.apostra.com/api/v2
AUTH='Authorization: Bearer <direct-admin-user-key>'
```

## Identity and credentials

Apostra issues each principal an opaque `principal_id` (for example
`prin_yq8Kd…`). That is the **only** identifier that identifies the principal.
It is a routing and audit handle: it is not a credential, and holding it grants
nothing.

Everything else attached to a principal is a **credential fact** — an OAuth
client ID, an API key ID, an issuer URL, a WorkOS organization ID, a domain, a
`brand.json` or JWKS URL, a signing key ID, a key fingerprint. Credential facts
identify a *credential*, never the principal. Rotating or replacing a credential
changes its facts and keeps the same `principal_id`; two credentials bound to
the same buyer agent share one `principal_id` and have different facts.

A principal also has a **kind** and a **lifecycle state**:

* **Buyer agent** — one automation your organization runs. It holds credentials,
  and you administer those credentials.
* **Organization operator** — your organization acting as itself, created the
  first time an organization credential calls Apostra. It is not a buyer
  agent and has no credentials to administer.
* **Lifecycle state** is `active`, `suspended`, or `retired`. A buyer agent is
  suspended when it has no active credential left; every call it makes is then
  refused until you add one. Its `principal_id` is retained for audit and is
  never reissued.

**Identity is not authority.** Holding a principal or a credential gives no
advertiser, account, spend, payment, or Terms access. Those come from separate
per-advertiser grants and are never implied by identity.

## Manage buyer agents in the console

In the **Agents** workspace, choose **API calls** in the left navigation to
open the existing Activity widget. It shows account-wide API traffic in the
main screen, alongside chat. API keys remain in **Settings**.

Activity remains available to account members under the existing access rules.
Accounts outside the Calls beta receive the existing Changes view instead;
moving the navigation does not enable the beta or grant call-detail access.

For agent access and credential administration, open **Settings → API keys →
Manage buyer agents**. This opens the existing management page at
`/<account-id>/buyer-agents`. It shows, for the organization you are signed in
to:

* each agent's name, connection status, advertiser access, and useful recent
  activity; purpose and operator information are labeled unavailable until an
  agent-scoped authoritative source supplies them;
* an agent detail page organized into **Overview**, **Access**, **Connections**,
  **Notifications**, **Activity**, and **Settings**;
* safe credential facts and management actions in Settings.

The Overview answers what the agent is, who operates it when that information
is available, whether it is connected, what it can do, and what needs
attention. Access lets an administrator select exact advertisers and choose
**Read** or **Read and write** for each one. Connections contains Apostra connection and organization-owned reporting and data resources.
Notifications contains machine-event subscriptions into the agent. It does not
turn an organization-owned Slack, email, or inbox destination into a
subscription. Activity keeps platform state effects distinct from exact actor
records for actions taken by human administrators.

The organization roster's separate **operator principal** represents the
organization acting as itself. It is not the signed-in human administrator, the
operator of every agent, or an identity attributed to an agent. Until an
agent-scoped source provides authoritative operator metadata, the agent Overview
says that operator information is unavailable.

### Overview and Settings

The **Overview** tab shows the current agent name, lifecycle state, registered
date, active credential count, advertiser access summary, connection status,
and readiness information. These facts come from the agent's current management
record. If that record is temporarily unavailable, the console shows an
unavailable state instead of showing older or partial facts as current. You can
refresh the tab to try the read again.

The **Settings** tab lets an administrator rename the agent and manage its
lifecycle. Suspending an agent and retiring it require typing the agent's exact
current display name before the action is available. Resuming a suspended agent
does not require typed confirmation. Retiring an agent is terminal and cannot
be reversed.

Internal identifiers, protocol versions, configuration versions, destination
generations, proof mechanics, and raw technical state are available only in
the collapsed **Advanced diagnostics** section.

The page never shows a credential secret. A secret is delivered exactly once, by
the issue or rotate response itself, and cannot be retrieved afterwards — not by
the console, not by the API, and not by support.

From that page you can:

* **Issue a credential** — Apostra creates a new identity-only OAuth client
  and shows its secret once.
* **Register an existing credential** — bind an API key or OAuth client your
  organization already owns to a new buyer agent. Registration does not change
  what that credential could already do.
* **Rotate an API key** — Apostra creates the replacement, shows its secret
  once, and retires the current key.
* **Revoke a credential** — the credential stops authenticating immediately.

Rotation and revocation both retire a live credential, so both require you to
retype the buyer agent's name before the action is available. Before you
confirm, the console states what will happen — including, for revocation,
whether this is the buyer agent's **last active credential**. Revoking the last
active credential suspends that buyer agent, and revocation cannot be undone:
the same credential can never be registered again.

## Grant advertiser access

Open an agent's **Access** tab and add advertisers from your organization.
Choose **Read** when the agent should only inspect that advertiser, or **Read
and write** when it may submit changes. Saving reconciles the complete list;
removing an advertiser revokes that agent's access to it. A new advertiser is
never granted automatically.

The console sends the revision it most recently read with every save. If another
administrator changes access first, the server returns a conflict. The console
does not retry or merge that request: reload the current grants, review them,
then re-apply the intended changes.

## Apostra-hosted runtime sessions

Hosted buyer agents use the same principal lifecycle and exact advertiser
grants described above. During the controlled rollout, hosted creation remains
unavailable in the console unless Apostra has enabled and provisioned the
installation for your organization.

When an authorized person messages a provisioned hosted agent, Apostra
creates or resumes a conversation that is isolated to the exact installation
and caller. Conversation history is retained by Apostra rather than by the
runtime cell. The agent can read a bounded page of that history through its
conversation tools. Opaque session handles are not credentials and cannot be
used by another installation or caller.

Each request receives a short-lived connection credential from Apostra.
The runtime cannot supply its own user identity or expand its advertiser
access. For every advertiser operation, Apostra checks both the agent's
current grant and the verified caller's current permission. The narrower result
wins. Suspending the agent, revoking its advertiser grant, or removing the
caller's permission takes effect on the next request, even if an earlier
conversation or connection credential still exists.

Apostra rejects replayed caller assertions and fails closed when the
installation, caller, session, or current authority cannot be verified. These
controls do not grant proactive messaging, payment authority, Terms acceptance,
or access to an advertiser that was not explicitly granted.

## Manage an agent through MCP

On the V3 MCP endpoint, a directly authenticated organization administrator
uses `save_buyer_agent` to create an external agent, rename an existing one,
replace its exact advertiser access, or change lifecycle state. The response
uses the same `{ kind: "buyer_agent", object: ... }` projection as
`get({ kind: "buyer_agent", id })`.

To create an external agent, omit `id` and provide `displayName`. Creation is
standalone: use the returned `id` in a later call to set access or lifecycle.

```json theme={null}
{"displayName":"Planning agent","kind":"external"}
```

To update one agent, provide its `id` and exactly one change: a new display
name, access, or lifecycle. Access is a complete replacement and must carry
the `expectedAccessRevision` returned by a previous read. A stale revision is
a conflict; re-read before trying again.

```json theme={null}
{
  "id":"<principal-id>",
  "access":{
    "expectedAccessRevision":4,
    "advertisers":[{"advertiserId":"42","role":"READ_WRITE"}]
  }
}
```

Lifecycle changes are state-fenced. `suspend` expects `active` and requires
the exact current display name as `confirmationText`; reversible `resume`
expects `suspended` and does not require confirmation; terminal `retire`
requires the exact current display name as `confirmationText`. Hosted-agent
creation is not yet available and is rejected without creating an installation.

## Register a Slack channel binding

This capability is available only when your organisation has been enrolled in
the `buyer-agent-channel-bindings` feature flag. While the flag is off, the
create endpoint is unavailable, but you can still list and revoke existing
bindings. You can bind an active agent before it has a chat endpoint. Messages
to that channel receive an in-thread notice until the agent is reachable.

A directly signed-in organization administrator can use the REST API to bind a
an internal, non-shared Slack channel in their organization's workspace to an active
buyer agent. An @mention starts a bound conversation; later replies in that same
thread continue it. Other channel messages are not forwarded. The agent receives
the message without a Slack sender identity in this release. Its reply is the
agent's text posted through the organization's existing Slack installation. A
Slack connection does not
grant advertiser access, notification delivery, or permission to post
proactively.

List connections with `GET /buyer-agent-principals/<principal-id>/channel-bindings`.
Create one with `POST` to the same URL and `{ "provider": "slack", "teamId":
"<slack-team-id>", "channelId": "<slack-channel-id>" }`. Delete
`/buyer-agent-principals/<principal-id>/channel-bindings/<binding-id>` to revoke
the connection. Revocation takes effect for the next inbound message and the
record remains available for audit.

A Slack channel can have one active buyer-agent binding in its workspace. Revoke its binding
before assigning that channel to another agent.

## Read your organization's principals

```bash theme={null}
curl "$BASE/buyer-agent-principals" -H "$AUTH"
```

The response has two separate collections. `operator` is your organization
operator principal, or `null` if one has not been created yet — it carries no
credentials. `buyerAgents` lists each buyer agent with its `principalId`,
`principalKind`, `displayName`, `lifecycleState`, `registeredAt`,
`activeCredentialCount`, deprecated empty `authority`, `access`, and its
`credentials`. `access` contains `accessRevision` and the current advertisers,
each with an `advertiserId`, display name, and `READ` or `READ_WRITE` role.

Each credential carries `credentialType`, `bindingState`, `boundAt`,
`lastVerifiedAt`, `retiredAt`, and an `administrationHandle` — the one
non-secret selector the rotate and revoke calls accept
(`{"type":"api_key","serviceTokenId":"…"}` or
`{"type":"workos_m2m","subject":"…"}`). It is `null` for a credential type this
endpoint does not administer.

To replace the complete advertiser list, send the `accessRevision` from that
last read unchanged as `expectedAccessRevision`:

```bash theme={null}
curl -X PUT "$BASE/buyer-agent-principals/<principal-id>/access" \
  -H "$AUTH" \
  -H 'Content-Type: application/json' \
  --data '{
    "expectedAccessRevision": 4,
    "advertisers": [
      {"advertiserId":"<advertiser-id>","role":"READ"},
      {"advertiserId":"<other-advertiser-id>","role":"READ_WRITE"}
    ]
  }'
```

An HTTP `409` means the revision is stale. Fetch the principal list again and
re-apply the intended list; never retry the same body automatically.

The read returns no secret, and it does not return issuer URLs, WorkOS
organization IDs, access client IDs, domains, `brand.json` or JWKS URLs, signing
key IDs, or key fingerprints. Those are credential facts, not identity, and this
surface does not carry them. Responses are marked `private, no-store`.

## Set advertiser access

An administrator grants access to exact advertisers through the buyer-agent
principals REST API. `READ` permits advertiser reads; `READ_WRITE` permits
reads and writes.
Access is never inferred from registration, a credential, organization
membership, or an account switch.

The API replaces the full exact set in one request. First read the current
`access.accessRevision`, then send it as `expectedAccessRevision`; a concurrent
change returns `409` and you should reload before trying again.

```bash theme={null}
curl -X PUT "$BASE/buyer-agent-principals/<principal-id>/access" \
  -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"expectedAccessRevision":3,"advertisers":[{"advertiserId":"42","role":"READ"}]}'
```

The response returns `access.accessRevision` and the resolved advertiser
names. Removing an advertiser revokes its active grant; changing a role
records a revocation and a new grant for the new role. Each advertiser
operation checks the active grant again. Advertisers delegated from another
customer are not available to a buyer agent's request-time API operations.

### What a grant allows

* Grants name advertisers your organization owns. The request is rejected
  when it names an advertiser owned by another customer or an archived
  advertiser.
* A buyer agent works under one selected advertiser at a time, chosen with
  the `X-SCOPE3-SEAT-ID` header on REST or the advertiser selection on MCP.
  Selecting an advertiser without an active grant is refused.
* `READ` allows reads of that advertiser's campaigns, creatives, media buys,
  and reporting. Any write with a `READ` grant is refused with
  `agent_read_only`. `READ_WRITE` allows writes as well.
* Objects addressed by id (a campaign, creative, or media buy) must belong to
  the selected advertiser; lists that are not filtered by advertiser return
  only the selected advertiser's objects. The only list a buyer agent can read
  without a selection is its own granted advertisers.
* A buyer agent cannot switch accounts. `X-SCOPE3-CUSTOMER-ID` on REST and the
  account-switch tools on MCP are refused for buyer agents.
* Notification delivery re-checks the grant on every attempt, so revoking a
  grant also stops that advertiser's notifications reaching the agent.

## Review connections and readiness

Open an agent from the **Agents** page. The console discovers the seller-account
contexts already associated with that exact agent and organization and presents
them by name. It never asks you to enter a numeric account ID. Choosing a named
context grants nothing: the signed-in administrator must already be allowed to
administer the organization, and the read remains isolated to that exact agent
and seller account.

The discovery response is available to directly authenticated organization
administrators:

```bash theme={null}
curl "$BASE/buyer-agent-principals/<principal-id>/administration-contexts" \
  -H "$AUTH"
```

It returns only the named contexts backed by current standing configuration for
that principal. It does not expose or infer other organizations' accounts.

The default experience translates configuration and readiness into customer
language. It deliberately does not redisplay endpoint URLs, destination
locations, recipient identities, extension data, proof artifacts, or credential
material. Advanced diagnostics preserves official readback and proof state for
support and engineering use.

Use the principal administration read for one buyer agent and Seller Account:

```bash theme={null}
curl "$BASE/buyer-agent-principals/<principal-id>/administration?storefrontId=<seller-account-id>" \
  -H "$AUTH"
```

`readback` preserves the four official AdCP states: `current`, `recognized`,
`unconfigured`, or `failed`. A current response carries the exact configuration
and version returned by `get_principal`. The authenticated API readback carries
that configuration; the console renders only its safe summary. The supplemental `proof` section names
each notification subscriber's explicit `pending`, `verified`, `failed`, or
`stale` state. Reporting destinations are listed by generation and pattern with
their recorded setup and proof states. Apostra does not infer proof success
from a configured URL, destination, domain, credential, or prior result.
If no recognized proof observation exists, `proofState` is `null`; absence or
malformed evidence is not promoted to a state.
Notification subscriber identifiers follow the official AdCP SDK limit of 64
characters. Both official configuration request schemas enforce that bound
before a task handler runs, and the configuration service enforces it again
before persistence. The administrative deactivation control retains the wider
legacy storage-key range only so an unexpected pre-existing key can be disabled;
it does not make that key valid configuration, identity, authority, or proof.
The read returns every current destination plus the newest prior generations,
up to 100 records total, and sets `reportingDestinationsTruncated` when older
generations exist.

Two histories stay separate:

* `stateFeed` contains `principal.changed` transitions that require a fresh
  `get_principal` read. Seller-driven proof and destination-state changes are
  recorded there; your own successful `sync_principal` calls are not. The read
  returns the latest 100 and sets `stateFeedTruncated` when older transitions
  exist. The seller persists the transition and its stable logical notification
  together, so an interruption before webhook handoff is recovered without
  turning the webhook into state or authority.
* `auditFeed` contains human administration changes and the exact authenticated
  person who made each change. It does not include endpoint URLs, destination
  locations, proof artifacts, or secrets. The read returns the latest 100 and
  sets `auditFeedTruncated` when older records exist.

The readiness checklist reports five independent gates: registration, an active
credential, current configuration with recorded proof, the exact advertised
capability, and separate business authority. Every gate must be ready. A
principal ID, credential, domain, destination, or proof result never satisfies
the business-authority gate and never grants advertiser, account, spend,
payment, or Terms access.

These gates do not substitute for one another. SSO authenticates the person
installing or administering the integration. A workload credential identifies
the buyer agent. Only an explicit principal × advertiser or account grant can
authorize business operations, and spend, payment, and Terms authority remain
separate. The state screen can report those boundaries; it cannot create or
approximate a missing grant.

## Stop a destination, subscriber, or principal

Send an administration control to
`POST /buyer-agent-principals/<principal-id>/controls`. Every control requires
`confirmationText` matching the buyer agent's stored display name exactly and a
current state fence. A successful response is an applied receipt containing the
resulting configuration version or lifecycle state; it does not depend on a
second read after the control commits. Refresh the administration read
separately to load the complete current view.

* `deactivate_destination` requires `storefrontId`, `destinationId`, its exact
  `generation`, and `expectedConfigurationVersion`.
* `deactivate_subscriber` requires `storefrontId`, `subscriberId`, and
  `expectedConfigurationVersion`.
* `suspend`, `resume`, or `retire` applies to the agent as a whole and requires
  `expectedLifecycleState`. `suspend` and `retire` require `confirmationText`;
  reversible `resume` does not. Use the agent-wide `lifecycle-controls`
  endpoint without a seller-account context. The original V2 `controls` request
  and receipt continue to require `storefrontId` for existing clients.

Only a directly signed-in organization administrator can use these controls.
Cross-organization or cross-principal targets, stale versions or generations,
support and impersonated sessions, delegated or simulated sessions, API keys,
OAuth clients, and ambiguous actor identity are refused.
Configuration controls carry one five-second database deadline across
authorization, state validation, and the committed mutation.

The console exposes only controls backed by current contracts. Deactivating a
destination or machine subscriber, pausing an agent, and retiring one all
require you to retype the exact agent name. Configuration controls submit the
reviewed version and, for a destination, its generation. Lifecycle controls
submit the lifecycle state you reviewed, so a stale screen cannot silently
change newer state.

## Issue a credential

Send a new UUID idempotency key for this issuance. You may supply
`replacesM2MSubject` only when replacing the directly preceding issued M2M
credential for the same automation; Apostra verifies that lineage itself.

```bash theme={null}
curl -X POST "$BASE/buyer-agent-principals/issue" \
  -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"displayName":"Reporting automation","idempotencyKey":"<uuid>"}'
```

Save `credential.clientSecret` immediately in your secret manager. It is shown
once and cannot be retrieved, regenerated, or returned by a retry. The returned
client ID is the automation's identity; it has no action permissions. It can
authenticate but receives `403` from business APIs, including advertiser,
account, spend, payment, Terms, and MCP operations.

## Reconcile an interrupted issuance

If issuance reports `ISSUANCE_RECONCILIATION_REQUIRED`, repeat the **same**
request with the same idempotency key after the transient failure has cleared.
Apostra locks the durable issuance record, proves that no registry binding
exists for its recorded client ID, discovers the deterministically named WorkOS
application, and only then compensates it. A completed or otherwise uncertain
binding is never deleted automatically.

Do not create another request or use a new idempotency key until that retry has
settled. If it reports that reconciliation is still required, retain the request
ID and contact support; do not assume the client is active or delete it through
the generic M2M endpoints.

## Rotate a credential

### API key

Rotate a registered API key with only its current token ID. The server creates
the successor with the same customer, organization, workload identity, and the
next credential version; callers cannot provide a successor ID or lineage.

```bash theme={null}
curl -X POST "$BASE/buyer-agent-principals/rotate" \
  -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"type":"api_key","currentServiceTokenId":"12345"}'
```

The response includes `credential.clientId` and `credential.clientSecret` once.
Save both before deploying the successor. The previous key is retired only as
part of the successful registry rotation. If the operation fails, do not create
a manually linked replacement through `/service-tokens`; retry or contact
support with the current token ID.

### WorkOS M2M

To replace an issued WorkOS M2M subject while retaining its buyer-agent
identity, issue the successor with `replacesM2MSubject`; the replacement is
accepted only when the server-recorded workload lineage and direct predecessor
match. This is a new-client rotation, not a generic M2M secret mutation.
Identity-only applications are intentionally excluded from
`/m2m-applications` list, secret, and delete operations so they cannot bypass
direct WorkOS-admin authorization and registry audit.

## Revoke a credential

To permanently stop an issued or registered credential from authenticating,
use `POST /buyer-agent-principals/revoke`. Revocation is immediate and cannot
be undone by registering the same credential again.

If this was the buyer agent's last active credential, the principal becomes
`suspended` and every call it makes is refused until you issue or register
another credential for it. The `principal_id` is retained for audit, so the
buyer agent's identity and history survive; only its ability to authenticate
stops.

```bash theme={null}
curl -X POST "$BASE/buyer-agent-principals/revoke" \
  -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"type":"workos_m2m","subject":"<client-id>"}'
```

For an API key, use `{ "type": "api_key", "serviceTokenId": "12345" }`.
