Skip to main content
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 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 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 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.

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).
  • 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. 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.

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); 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: 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 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 says what each severity means. How certification treats live traffic is in How certification works.