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

# Agent requirements: declarations and behaviour

> The declarations Apostra requires of a sales agent, the behaviour findings its live calls produce, and how to fix each one.

This page explains what Apostra checks about a sales agent before it sends the
agent buyer work, and how to fix each finding. Every finding on the agent read
(`get` with `kind: "agent"`) links to a section here. It is for the developer of
the agent. [Create an agent in a Developer account](/v3/developer-agents#findings-what-needs-your-attention)
lists every finding code and what it means.

We use evidence from our own calls to your endpoint: the
`get_adcp_capabilities`, `sync_accounts` and `get_products` calls we make, and
the requests we send while discovering inventory and handling buys. We measure
the response your agent sends, its timing, its task progress and whether the
products it returns honour the request. A self-reported claim is not proof.
[How certification works](/v3/certification) explains what makes an agent ready,
what each use case judges and what every result means.

## Required declarations

### AdCP 3

**What it is.** Your `get_adcp_capabilities` response must include major
version 3 in `adcp.major_versions`. Responses must validate against the schema
for the version we negotiate.

**Why buyers care.** A buyer needs the same contract that your Agent says it
supports, so a discovery result or buy does not fail when it reaches your
system.

**How we measure it.** We call your capabilities endpoint and validate the
responses we receive at the negotiated version.

AdCP 3 is enough to certify today. From 1 February 2027, four capabilities
become required. See [Getting ready for AdCP 4.0](/v3/adcp-4-readiness)
for the schedule, the checks we run and what to change.

### A supported billing model

**What it is.** Declare `agent`, `operator`, or both in `supported_billing`.
This is a seller-wide declaration, selected for each account in
`sync_accounts`. We review `advertiser` billing case by case.

**Why buyers care.** The billing model determines who can operate the buy and
settle it without a manual intervention after the buyer is ready to transact.

**How we measure it.** We read the billing declaration from the account and
capability responses we call, rather than relying on a separate statement from
your team. We then check that your Agent accepts the billing models it declares:
a `sync_accounts` request for a declared model that your Agent refuses shows as a
[Declarations match behaviour finding](/v3/agent-requirements#declarations-match-behaviour-finding).

### Takes buyer account requests

**What it is.** Your Agent must list both `sync_accounts` and `list_accounts`
and must not require operator authentication
(`account.require_operator_auth: true`). Buyers declare their own accounts:
`sync_accounts` receives a buyer's account request, and `list_accounts` lets us
read the result back.

**Why buyers care.** Every buy runs on an account. A buyer can only start
buying from you once your Agent can open an account for them.

**How we measure it.** We read the tool list from your
`get_adcp_capabilities` response. The Agent page shows the result as **Takes
buyer account requests** in its required capability declarations.

**What happens when it is missing.** When your Agent is the only source behind
a storefront, we cannot open an account for a new buyer, so:

* Certification skips with `seller_sandbox_account_failed` (or
  `seller_operator_auth_unsupported`), because it sets up its test account the
  same way.
* An advertiser that does not already have an active account with you no longer
  sees your products in discovery and cannot book with you.
* Advertisers whose accounts you already accepted keep discovering and buying
  as before.

Add both tools (and accept the connected credential) and refresh your Agent's
capabilities to open your storefront to new buyers again.

### Creative formats we supply

**What it is.** At least one Product must declare a format. It can declare a
canonical `format_options[].format_kind` that we support (`display_tag`,
`video_vast`, or `audio_daast`), or legacy `format_ids`.

**Why buyers care.** A buyer can only place a buy if Apostra can deliver a
creative that the Product accepts and can measure.

**How we measure it.** We inspect the Products returned by our own
`get_products` calls.

## Behaviour findings and how to fix them

Each row in the Agent's **Agent-wide behaviour** list links to one section below.
Every section gives the threshold, the usual causes, and the next step.

We apply each threshold to every Source bound to the Agent separately, over its
own seven days. The Agent-wide row shows the worst result among those Sources
and adds up their counts. So "20 handoffs were still unfinished across one
Source" means that Source has 20, not that the Agent has 20 in total. Open
**Source detail** to see which Source needs work.

A finding that says **Not yet evaluated** has no matching calls in the last
seven days, so it is neither passing nor failing. It clears after we make the
kind of call named in its section.

### Responsiveness finding

**Threshold.** For each Source, over seven days:

* **Blocker:** more than 5% of at least 20 `get_products` calls timed out. A call
  times out when it returns neither a result nor a task handle before the
  response deadline. By default that deadline is 45 seconds, counted from the
  moment we send the request. A buyer can set a shorter or longer budget.
* **Flag:** the p95 time to answer `get_products` is over 10 seconds, and at least
  one call that took more than 10 seconds returned no task handle.

**Usual causes.**

* The Agent builds the whole catalog during the request, for example by calling
  the upstream ad server live on every `get_products`.
* The upstream ad server is slow or rate limiting the Agent.
* Connection setup and cold starts use part of the deadline.
* The Agent does not use a task handle, so a slow request can only succeed or time
  out.

**Next step.** Find the slow or timed-out `get_products` calls in your own logs
for the window. If the Agent cannot answer within about 10 seconds, return
`submitted` or `working` with a task handle straight away and finish the work in
the background. Where you can, serve the catalog from a cache you refresh on a
schedule.

### Async completion finding

**Threshold.** A handoff is **stale** when your Agent answered `submitted`,
`working` or `pending`, and five minutes after our request it had not reported a
final status. Per Source: one or two stale handoffs is a **flag**; three or more
is a **blocker**. The five minutes is fixed today and is not a per-task deadline
you can set.

A final status is `completed`, `failed`, `rejected`, `canceled` or
`input-required`. A `working` update does not count. A task that finishes late
stops counting at the next evaluation, once we have recorded its final status.

**Usual causes.**

* The job was lost after the Agent returned the task handle, for example because
  of a restart, a deploy, or a queue that drops work.
* The Agent finished the work but never recorded the final status where polling
  reads it, or never sent it to the webhook.
* The webhook call failed, for example on a wrong URL or an expired credential,
  and was not retried.
* The work routinely takes longer than five minutes and only sends `working`
  updates.

**Next step.** List the task ids that were still open after five minutes in your
own logs. For each, check that a final status is stored and that polling returns
it. Check that the webhook call succeeds, and retry it when it fails. If a job
needs longer than five minutes, it still counts as stale until it reports a final
status, so report `input-required`, `failed` or `completed` as early as you can
rather than only `working`.

### Call success rate finding

**Threshold.** Over seven days, for each Source, the share of our calls that
failed for a reason other than a timeout. A call fails when your Agent returns an
error, rejects the request, or cancels it. **Flag** above 2%. **Blocker** above
20%, or when every call failed. This finding is **Not yet evaluated** until we
have made at least one call.

**Usual causes.**

* Validation that is stricter than AdCP, so your Agent rejects a valid request.
* Credentials for the upstream ad server that have expired or lost access.
* An account that does not exist or is not active on your side.
* An unhandled error inside the Agent, returned as a server error.

**Next step.** Group the failed calls in your own logs by error code and fix the
most common one first. A single rejected field or an expired credential usually
explains most failures.

### Honours request finding

**Threshold.** We do not calculate this finding yet, so it never shows **Flag** or
**Blocker** today. When we do, any breach will be a flag and three or more
within the seven days will be a blocker.

**Usual causes.** Products returned outside the property list, channels or
countries we sent, for example because the Agent ignores a filter it does not
support but did declare.

**Next step.** No action is needed for this finding today. Make sure returned
Products respect the property list, channels and countries in each request, and
that you declare only the targeting you honour.

### Account references resolve to one account finding

**Threshold.** Per Source: `ACCOUNT_AMBIGUOUS` returned once or more is a **flag**.
It is a **blocker** when it appears on three or more separate days in the seven
days.

**Usual causes.**

* Two accounts on your side share the same brand domain and operator, often
  because `sync_accounts` created a duplicate (see
  [Account sync is idempotent finding](/v3/agent-requirements#account-sync-is-idempotent-finding)).
* The lookup matches on brand alone and ignores the operator or the sandbox flag.

**Next step.** Merge or deactivate the duplicate accounts so each key matches one
account. Return an `account_id` on each account from `sync_accounts`; we then
address the account by id instead of brand and operator.

### Payment terms finding

**Threshold.** Counted for accounts we requested with agent billing and
`payment_terms`. A missing `billing` or `payment_terms`, or an existing account
kept on different terms, is a **flag**. A new account returned active on
different terms is a **blocker**. The full rules are in
[Return and honour payment terms on agent-billed accounts](/v3/agent-requirements#return-and-honour-payment-terms-on-agent-billed-accounts).

**Usual causes.**

* The Agent gives every new account its own default terms and ignores the terms
  we request.
* The account API your Agent calls cannot store payment terms.
* The response leaves out `billing` or `payment_terms`.

**Next step.** For each agent-billed account we request terms for, either accept
the requested `payment_terms` or reject the account. Never return it active on
other terms. Return `billing` and `payment_terms` on every such account in
`sync_accounts` and `list_accounts`.

### Account sync is idempotent finding

**Threshold.** Per Source: a repeat `sync_accounts` that creates a second account
for the same brand domain, operator and sandbox flag, or a `list_accounts`
roster that lists one key more than once, is a **flag**. It is a **blocker** when
this appears on three or more separate days in the seven days.

**Usual causes.**

* The Agent keys accounts on a generated id, or on brand alone, instead of brand
  domain, operator and sandbox flag.
* A retry after a timeout runs the create again.
* The sandbox flag is left out of the key, so a sandbox account and a live
  account collide, or the reverse.

**Next step.** Look up the account by brand domain, operator and sandbox flag
before creating it. When it exists, return it with `action: unchanged` or
`updated` and the same `account_id`. Merge the duplicates you already hold. Once
each key is listed once, the finding clears as the seven-day window moves on.

### Declarations match behaviour finding

**Threshold.** Today this finding checks billing. We count each `sync_accounts`
request we sent in the seven days for a billing model you list in
`supported_billing`. A request your Agent refused with
`BILLING_NOT_PERMITTED_FOR_AGENT` or `BILLING_NOT_SUPPORTED` is a mismatch, whether
the code came on the call itself or on the account in your answer. Any mismatch
is a **flag**. A mismatch seen on three or more separate days is a **blocker**.
When we sent no request for a billing model you declare, the finding is not
evaluated.

**Usual causes.**

* `supported_billing` lists a billing model, such as `agent`, that your platform
  is not set up to accept for buyers.
* The billing model is accepted only for some buyers or operators, but is
  declared for all.

**Next step.** Accept every billing model you list in `supported_billing`, or
remove the ones you refuse. Buyers choose a billing model from your declaration,
so a model you declare and then refuse leaves their account unopened. Once your
Agent stops refusing a declared model, the finding clears as the seven-day window
moves on. See [Keep capabilities current](/v3/agent-requirements#keep-capabilities-current).

### Machine-readable errors finding

**Threshold.** Over seven days, any error we receive without a recognised AdCP
error code is a **flag**. This finding is never a blocker on its own. Timeouts do
not count here.

**Usual causes.**

* The Agent returns plain text, an HTTP status or a stack trace in place of an
  AdCP error.
* The Agent passes through the upstream ad server's error, or uses its own
  code that AdCP does not define.

**Next step.** Map every failure path to the nearest AdCP error code and return
it with a message. Start with the errors that appear most in your logs.

## Practices behind the findings

### Return and honour payment terms on agent-billed accounts

**What it is.** When we ask for an account with `billing: "agent"` and
`payment_terms` (for example `net_60`), do one of two things: accept those
terms, or reject the account. Never return the account as `active` on
different terms. On every account in your `sync_accounts` and `list_accounts`
responses for an account we requested this way, return `billing` and
`payment_terms` so we can see what you hold. AdCP states that the seller
"must either accept these terms or reject the account — terms are never
silently remapped."

**Why buyers care.** The buyer's organization chose its payment terms. An
account on other terms means invoices that don't match what they agreed.

**What Apostra does when an Agent doesn't.** We refuse a new account that
comes back on other terms. It is not activated, and it is held for the buyer
to review the seller's terms. An existing account stays usable, and the buyer
sees a warning that names both the requested terms and the terms your Agent
kept.

**How we measure it.** We compare the accounts in your `sync_accounts` and
`list_accounts` responses over the last seven days with the terms we
requested. Only an account we requested with agent billing and `payment_terms`
in that window counts. An account we requested without terms is on your default
terms, as AdCP says, and isn't counted. An account we never requested in the
window isn't counted either, and a response from before our request doesn't
count against the account. We don't assume what your default terms are. For a
counted account, a missing `billing` or `payment_terms` is a flag, and an
active account on different terms is a blocker when your Agent created it or
activated it on those terms, and a flag when your Agent already held it. Your
Agent already held it when its `sync_accounts` response reports
`action: unchanged`, or `action: updated` after an earlier response held the
account active. A response that reports `action: created` or no `action` counts
as a new account. An account that was a blocker at any point in the seven days
stays one. A counted account whose listing has no `billing` counts as missing
`billing`. We
count accounts, not responses: an account is identified by its brand domain,
operator and sandbox flag (and its brand id and operator unit, when you return
them), however often it appears in your responses. An
account counts as missing a field only while its latest response in the window
lacks it, so it stops counting once your next response is complete. From
`list_accounts` we read only your most recent roster, which is every page of
your latest complete listing, so an account that has dropped out of it isn't
counted. An account
returned on different terms stays counted for the seven days. This
check reports today and does not yet stop new buys.

### Sync accounts idempotently

**What it is.** The account key is the brand domain, the operator and the
sandbox flag. When we send `sync_accounts` for a key your Agent already holds,
return that same account with `action: unchanged` (or `updated`, if something
changed) and the same `account_id`. Create an account only the first time the
key appears. `list_accounts` lists each key once.

**Why buyers care.** A buyer syncs the same account again whenever their setup
changes. If each call creates a new account, the buyer's buys, approvals and
invoices spread across accounts nobody asked for, and the seller cannot tell
which one is current.

**How we measure it.** Over the last seven days we look for two things: two or
more `sync_accounts` responses that report `action: created` for one key under
different `account_id` values, and a `list_accounts` response that holds more
than one account for the same key and sandbox flag. We read a `list_accounts`
roster as every page of one listing, joined by `pagination.cursor`, and use the
latest listing whose last page returns `has_more: false`; a listing that stops
early isn't used. Seeing either is a flag.
Seeing it on three or more days is a blocker. A sandbox account and a live
account for the same domain are different keys. Once your Agent merges the
duplicates and lists each key once, the roster signal clears. This check
reports today and does not yet stop new buys.

### Declare AdCP channels on every Product

**What it is.** Every Product declares the AdCP media channels it sells in
`channels`, such as `display`, `olv`, `ctv`, or `streaming_audio`. A Product
that sells different channels through different formats can instead declare
`applies_to_channels` on each format option. A format type on its own, such as
a video or audio format, is not a channel, because several channels share a
format.

**Why buyers care.** Buyers filter discovery, route proposal requests, and
report delivery by channel. A Product with no channel is missing from
channel-filtered discovery, and its delivery reports with no channel.

**How we measure it.** We read `channels`, then `applies_to_channels`, on the
Products returned by our `get_products` calls. Storefront readiness counts the
Products that declare neither (storefront readiness reports it as the check **Product channels declared**), the agent read shows it as the advisory finding `declaration.product_channels`, and live-path
certification skips such a Product with the reason `missing_channels` rather
than certifying it on a guessed channel. When a Product relies on
`applies_to_channels` and only some of its format options declare it, the
other formats are skipped with the reason `missing_channels:<format_kind>`. This is a best practice: a
missing channel produces a warning and never blocks your Agent or fails its
certification. We do not refuse an external Agent's Products for missing
channels. AdCP is considering making `channels` required on every Product in
AdCP 4.0
([adcontextprotocol/adcp#7869](https://github.com/adcontextprotocol/adcp/issues/7869));
if it does, AdCP compliance will require it.

### Source reporting health

**What it is.** Every inventory source that uses your agent is expected to
deliver its delivery reports on time. Apostra reads each source's reporting
state as one of five values:

| State | Meaning |
| - | - |
| `waiting` | No report is due yet. |
| `healthy` | Every report currently due has been produced and has met the required delivery or reconciliation step. |
| `delayed` | A report is late, but automatic recovery is still running. |
| `action_required` | Automatic recovery has finished, or a named party needs to act. |
| `complete` | The source's scope is closed and every required final report is satisfied. |

**Why buyers care.** Buyers reconcile spend and delivery from these reports.
A report that does not arrive leaves the buy open.

**How we measure it.** We count the states across the agent's authorized
inventory sources. The finding `reporting.sources` is an attention finding when
any source is `action_required`, and passes when none is. Nothing stops: running
buys continue. The inventory sources page shows each source's own state. See
[Reliable Reporting](/v3/adcp-4-readiness#reliable-reporting-reliable_reporting)
for the capability that makes reports dependable.

### Keep capabilities current

**What it is.** Update capability, account, and Product declarations whenever
the upstream system changes what your Agent can do.

**Why buyers care.** Current declarations prevent a buyer from selecting
inventory that has changed, disappeared, or no longer supports a required
feature.

**How we measure it.** We use the current responses from our calls and compare
them with the behaviour we observe. If the two disagree, the declarations-match-
behaviour requirement applies.

## What a behaviour finding does

A **flag** is an `attention` finding: it tells you what we observed and how to
fix it. A **blocker** is `blocking` only for a check that Apostra enforces, and
only while enforcement is on for your account. For every other check it is an
`attention` finding, and running buys continue. `get` with `kind: "agent"` shows
each finding's `severity`, and
[Findings: what needs your attention](/v3/developer-agents#findings-what-needs-your-attention)
says what each severity means. How certification treats live traffic is in
[How certification works](/v3/certification#behaviour-on-live-traffic).


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