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

# Modular inventory source lifecycle

> Upload avails, inspect product projections, reserve capacity, prepare supported execution handoffs, and manage source-side human work

A **modular inventory source** is an operator-managed source composed from smaller modules: an inventory feed, an optional booking ledger, optional execution modules, and optional human work queues. The storefront still presents one buyer-facing sales-agent surface. The modules describe how that source gets inventory, prevents overbooking, books upstream supply, traffics campaigns, syncs creatives, and imports reporting.

Use a modular source when inventory and execution do not come from one fully compliant upstream sales agent. For example, a publisher may send a periodic avails spreadsheet, confirm bookings manually, traffic a campaign in an addressable TV system, and upload the final delivery report after the campaign ends.

<Note>
  Modular source operations are storefront-operator endpoints. Buyer agents do not call them directly. Buyers still transact through the storefront's AdCP media-buy workflow.
</Note>

<Warning>
  **Custom modular sources** are included with Premium and Enterprise through the Merchandising profile. Basic does not include custom composition. Standard managed integrations are included with every seller plan and do not require separate access. The stable internal feature key is `modular-sources`; customer-facing surfaces display **Custom modular sources**. Existing buyer fulfillment and source runtime operations continue after a downgrade to Basic. The restriction applies to new custom composition, module attachment, and demo resets that replace the source composition.
</Warning>

Storefront MCP clients open the same three portable surfaces with
`prepare_modular_source`, `get_modular_inventory_source_readiness`, and
`prepare_modular_avails_commit`. The Page and Tasks use named `api_call`
operations for their data and confirmed writes. The operator-facing operation
set also includes `list_modular_inventory_source_products`,
`get_modular_inventory_capabilities`, and
`search_modular_source_inventory_selectors`. Work-item detail, capacity and
booking changes, module configuration, and credential entry remain in
confirmation-gated trusted Tasks or the operator UI.

On the unified `/mcp/v3` storefront surface, use `open_page` instead of
calling those compatibility tools directly:

| `open_page.page`           | Optional context                     | Result                                                                                                        |
| -------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| `modular_source_setup`     | None                                 | Opens **Add a modular source**. Its confirmed completion operation is `create_feed_modular_inventory_source`. |
| `modular_inventory_source` | `sourceId`                           | Opens the readiness workspace for that source.                                                                |
| `modular_inventory_feed`   | `sourceId` and optional `sourceName` | Opens **Commit avails feed**. Its confirmed completion operation is `commit_inventory_feed`.                  |

These v3 page names launch the same portable MCP Apps used by other supported
hosts; they are not Murph-only shortcuts. Feed files stay in the host-to-Page
upload flow, so the model does not receive the feed bytes. Creating and
committing remain explicit, confirmation-gated writes.

All examples use the storefront base URL:

```bash theme={null}
export BASE=https://api.apostra.com/api/v2/storefront
export SCOPE3_API_KEY=scope3_<your_api_key>
```

## When to use a modular source

Set up your storefront first, then decide which source type each inventory
system should become:

| Source type             | Use it when                                                                                                                                                                         |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| External sales agent    | The vendor already runs a compliant AdCP sales agent that buyers can use through your storefront.                                                                                   |
| Ad-server-backed source | Apostra has a managed integration for your ad server, such as GAM, FreeWheel, SpringServe, or AdsWizz. Connecting these managed adapters is included and has no source-count limit. |
| Linked storefront       | Another Apostra storefront should supply products or execution.                                                                                                                     |
| Modular source          | Inventory, booking, creative, trafficking, and reporting live in different systems or human workflows.                                                                              |

A modular source is the right pilot shape when the seller can provide a feed of
wholesale inventory but cannot yet expose the whole lifecycle through one
upstream API.

<Card title="Open the fictional publisher test pack" icon="flask" href="/v2/setup/publisher-onboarding-example-pack">
  Copy complete CTV and display feeds, storefront materials, operating evidence,
  and a machine-graded buyer-brief corpus. Every property, format, price, and
  date is synthetic.
</Card>

A manual or back-office source is modular because no single system can answer
the complete AdCP workflow. A spreadsheet may describe what can be sold, an OMS
may record the booking, an ad server may execute it, and a person may move the
creative or final report between them. Modeling those responsibilities as
modules makes every supported and human-owned step explicit without claiming
automation that is not connected.

Replacing one module with an integration is different from replacing the whole
modular source with an external AdCP sales agent. A whole-source replacement
changes the publisher authorization target: before the seller depends on the
replacement inventory, each publisher authorizes the external agent's URL in
its own `adagents.json`. The modular source does not retroactively become an
Apostra authorization case. See [Changing from a modular source to an
external agent](/v2/concepts/identity-documents#changing-from-a-modular-source-to-an-external-agent).

## Modules

Each module owns one part of the lifecycle. A source can start with manual
modules and later replace a step with an automated integration.

| Module            | Purpose                                                                      | Typical input                                                               |
| ----------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Inventory feed    | Ingest wholesale avails and project buyer-facing products.                   | CSV, XLSX, or XLS file upload; JSON through `jsonText` or a Murph document. |
| Booking ledger    | Hold and book capacity so the storefront does not overbook what it controls. | Reserved and booked impressions keyed by `availId` and media-buy id.        |
| Booking execution | Confirm the upstream booking in an OMS, ad server, or human queue.           | OMS API details, booking owner, or required human task fields.              |
| Creative sync     | Map accepted buyer creative to the upstream system before go-live.           | Creative specs, approval owner, and upstream creative reference.            |
| Trafficking       | Push or manually enter campaign details in the execution system.             | Campaign name, advertiser, dates, products, budget, creative refs.          |
| Reporting import  | Bring back final delivery after the campaign ends.                           | Delivery export with delivered impressions and upstream references.         |

Every modular setup must account for these lifecycle responsibilities, even
when the current answer is a named human rather than an integration:

| Responsibility           | Decide during setup                                                                                                 |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| Catalog and avails       | Who supplies current sellable rows, prices, dates, capacity, and stable ids; how often they refresh.                |
| Booking and execution    | What reserves capacity, what confirms the upstream order, who traffics it, and which event means it is live.        |
| Creative                 | Who validates specifications and approvals, and how buyer creative receives an upstream reference.                  |
| Reporting                | Who exports delivery, which identifiers reconcile it to the booking, and when final data is available.              |
| Readiness                | Which required modules, credentials, runtime inputs, and test results must be complete before this source can sell. |
| Human in the loop (HITL) | Which steps create a work item, who owns it, what evidence completes it, and how quickly it must be handled.        |

Read a stage across four separate axes: **execution mode** (automated, human,
or hybrid), **configuration** (whether its module and credentials are set up),
**runtime input** (whether this transaction has supplied what the stage needs),
and **declared capability** (whether the source supports the operation at all).
Human ownership does not by itself mean setup is missing. A configured manual
stage can be ready for transactions while a particular media buy still creates
seller-owned work. Conversely, an unsupported capability is not a task and
requires no action until the source composition changes.

Open manual booking, creative-sync, and final-reporting work appears both in
the source workspace and in **Pending Operations**. Both surfaces use the same
buyer-customer, media-buy, source, and work-item identifiers; opening a Pending Operations row
focuses that exact work item. A blocked item remains seller-owned but says
**No action now** until its prerequisite clears.

The CitrusAd pilot is a **standard composition**, so sellers connect it as an
ad server rather than assembling modules. It uses a retailer-namespace catalog
module and a campaign-execution module behind one inventory source. It is a
standard managed integration included with every seller plan; its dedicated
ad-server route presents a simplified recipe instead of the advanced modular
builder. The storefront remains in `CHEF` routing mode; buyers see the
storefront, not a CitrusAd adapter or the private module graph.

The first recipe supports sponsored-product campaigns. Their creative is
rendered from retailer catalog data, so creative sync completes without an
asset upload. CitrusAd also offers static banners and Banner X, but those are
not exposed by this recipe until separate banner campaign and creative APIs are
implemented and verified.

Connect the standard recipe through
`POST /inventory-sources/ad-servers/citrusad`, then save the storefront-scoped
credential through
`POST /inventory-sources/{sourceId}/ad-servers/citrusad/credentials/{credentialKind}`.
These ad-server endpoints do not expose the private module graph.

## Module credentials

Credentials belong to one module instance on one storefront inventory source.
They are not process environment variables and are not shared across
storefronts. Raw `auth` values are written to the configured secret manager;
the API response contains status, scopes, expiry, and other non-secret metadata
only. Credential mutations require a storefront admin role or the dedicated
`StorefrontCredentialsAdmin` permission.

Create a credential with `POST`, rotate it with `PUT`, and revoke it with
`DELETE` at:

```text theme={null}
/inventory-sources/{sourceId}/modular/modules/{moduleInstanceId}/credentials/{credentialKind}
```

`credentialKind` is `API_KEY`, `BEARER`, or `OAUTH`. For example:

```bash theme={null}
curl -X POST "$BASE/inventory-sources/retail-modular/modular/modules/citrus-campaign/credentials/API_KEY" \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "auth": { "userKey": "<provider-user-key>" },
    "scopes": ["campaigns:write"]
  }'
```

Credential-shaped fields are rejected from module config and credential
metadata. Put all provider secrets under `auth`.

Storefront-level approval policies still apply outside the modular source. For
example, advertiser approval and creative review can happen before a modular
source asks a human to map the approved creative into an upstream ad server.

## Seller setup checklist

Send the [publisher onboarding starter
kit](/v2/setup/publisher-onboarding-starter-kit) before setup. It is the
reusable evidence checklist for identity, properties, inventory, pricing,
avails, booking, creative, trafficking, reporting, and recurring ownership.
It also includes `static-avails-feed:v1` compatibility CSV and JSON templates and
explains which evidence is needed for each readiness milestone.

Ask the seller for what they already have and request only the gaps. Media kits,
co-viewing studies, market-size notes, and screenshots remain useful context,
but they are not avails feeds unless they contain row-level sellable capacity
for a specific inventory scope and time window.

## The three setup surfaces in chat

Setting up and running a modular source is three surfaces, each with one
job. Ask Murph for any of them by name, or move between them from the
workspace.

| Surface                      | What it is for                                                                                                                                   | How it ends                                                      |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| **Add a modular source**     | Register a new source: its name, where its avails come from, and how often they arrive.                                                          | Closes when the source is created and opens its workspace.       |
| **Modular inventory source** | The place you return to for an existing source: how ready each lifecycle stage is, which modules are attached, and the work waiting on a person. | Stays open; refreshes as you work.                               |
| **Commit avails feed**       | Turn an avails export into sellable capacity.                                                                                                    | Closes when rows are committed and returns you to the workspace. |

Two things follow from that split:

* **Creating a source does not ask how you book, traffic, clear creative, or
  report.** Those are derived per lifecycle stage from the modules attached to
  the source, and shown in the workspace — see
  [Check readiness](#1-check-readiness). Every modular source starts with an
  avails-feed module and Apostra's booking ledger.
* **Nothing is offered to buyers until avails rows are committed.** A source
  with no committed rows is registered but has no sellable capacity.

## Preview buyer discovery from one source (read-only)

When **Get products** is **Ready** in the **Modular inventory source**
workspace, select **Open read-only discovery preview**. In the **Read-only
source discovery preview** Task, enter a representative buyer brief and select
**Run read-only preview**.
The Task shows only products that source would return through your storefront.

The preview rechecks that the source is active and **Get products** is still
**Ready** when you run it. If either check fails, the Task stops and asks you to
finish the source setup. It never includes products from another inventory
source.

The Task records the preview decision for later review. It does not create a
media buy, change live products or source setup, or turn on transacting. Brief
tests started outside a source workspace remain storefront-wide.

## Optional ad-server automation

Attaching ad-server automation to a modular source automates a lifecycle stage
for that same inventory and operating boundary. It does not create another
source. Add a parallel source only when it represents distinct inventory,
authorization, or operational ownership—not merely because trafficking moves
from a human workflow to an integration.

## Avails feed requirements

The first successful upload should be boring: one row per sellable line, with
stable ids and clear capacity semantics.

Required columns:

<Warning>
  The current `static-avails-feed:v1` parser is a production compatibility path,
  not the target Inventory Feed schema. Its exact `collectionId`,
  `collectionName`, and `collectionDescription` names are legacy
  grouping/container fields. Their values do not establish AdCP Collection
  identity. Do not model a monthly pool, placement, channel, or portfolio as an
  AdCP Collection to make a row fit.
</Warning>

| Column                | Meaning                                                                                                                                        |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `collectionId`        | **Legacy field name.** Stable seller-side id for the static-v1 compatibility grouping/container; not AdCP Collection identity.                 |
| `collectionName`      | **Legacy field name.** Seller-facing label for that compatibility grouping/container.                                                          |
| `formatOptions`       | Non-empty URL-free AdCP declarations for the inventory represented by the compatibility grouping, using `format_kind` plus canonical `params`. |
| `publisherProperties` | Non-empty AdCP Property selectors for the inventory represented by the compatibility grouping.                                                 |
| `availId`             | Stable seller-side id for this availability row. If missing, Apostra can derive one, but explicit ids are better.                              |
| `name`                | Seller-facing row name.                                                                                                                        |
| `startTime`           | ISO date or date-time for the availability window start.                                                                                       |
| `endTime`             | ISO date or date-time for the availability window end.                                                                                         |
| `impressionsCapacity` | Net sellable impressions available to this storefront.                                                                                         |

Optional columns:

| Column                                             | Meaning                                                                                                     |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `cpm`, `rateCpm`, or `priceCpm`                    | CPM for this row.                                                                                           |
| `currency`                                         | ISO currency code.                                                                                          |
| `channel` or `mediaChannel`                        | CTV, audio, OLV, display, home screen, podcast, etc. Descriptive only; it never determines creative format. |
| `targeting`                                        | JSON object with structured targeting facts.                                                                |
| `sourceMetadata`, `source_metadata`, or `metadata` | JSON object with source details.                                                                            |
| Any other column                                   | Preserved as source metadata when possible.                                                                 |

<Warning>
  The current modular avails-feed contract is impression-based: it requires
  `impressionsCapacity` and can carry an optional CPM. It does not yet represent
  click/engagement pricing, a whole-flight flat rate, or time-based sponsorship
  pricing. Do not convert those commercial models into invented impressions or
  CPMs. Keep the original rate and capacity semantics in the source material and
  ask Apostra to add the missing contract support before committing those rows as
  sellable avails.
</Warning>

If the seller only has gross capacity, include one of `avails`, `impressions`,
`monthlyAvails`, `monthlyVideoAvails`, or `monthlyHomeScreenAvails`, plus
`bookedImpressions`, `upstreamBookedImpressions`, `booked`, or
`reservedImpressions`. Preview will calculate net capacity as:

```text theme={null}
impressionsCapacity = max(0, gross capacity - upstream booked capacity)
```

If upstream booked capacity exceeds gross capacity, preview rejects the row
instead of silently clamping it to zero.

Upload CSV, XLS, or XLSX files. Send JSON as `jsonText`, or upload it as a Murph
document and pass its `documentFileId`. Pasted `csvText` can parse tab-delimited
text, but `.tsv` is not a supported file-upload format; convert it to CSV first.
Impression-count fields can use plain numbers or common suffixes such as `150m`,
`25 million`, or `1.5B`. Money fields such as CPM must use an ordinary monetary
number; a magnitude suffix such as `5m` is rejected.

```csv theme={null}
collectionId,collectionName,availId,name,startTime,endTime,impressionsCapacity,channel,formatOptions,publisherProperties,cpm,currency
compat-publisher-ctv-group,Legacy group: Publisher CTV,publisher-ctv-2030-01,CTV January,2030-01-01,2030-02-01,150000000,CTV,"[{""format_option_id"":""ctv-vast-30"",""format_kind"":""video_vast"",""params"":{""duration_ms_exact"":30000}}]","[{""selection_type"":""all"",""publisher_domain"":""publisher.example""}]",24.00,USD
```

Copy the `static-avails-feed:v1` compatibility CSV or JSON template from the
[inventory source input guide](/v2/setup/publisher-onboarding-starter-kit#static-avails-feedv1-compatibility-templates).

## Murph walkthrough

Murph should not force a seller into a blank template. A good setup conversation
looks like this:

1. Ask the seller to upload what they have: deck, media kit, rate card,
   publisher list, avails export, booking export, creative specs, reporting
   export, or screenshots.
2. Classify the material. Keep market context as context; do not commit it as
   avails.
3. Ask for missing row-level avails if the seller only supplied market scale.
4. Create the modular source only after the operator confirms the
   source name and lifecycle plan.
5. Preview the feed. Show normalized rows and whether capacity is net or
   gross-minus-booked.
6. Commit only after explicit confirmation.
7. Run a small test reservation and validate the handoff requirements. Only run
   finalization when the source has a real execution module or a configured demo
   execution module.
8. Route unsupported integrations or missing systems into the normal Murph
   escalation flow so the need is tracked centrally.

For example, a note that says "150 million monthly video avails and 1.5 billion
home-screen avails" is not enough to launch. Murph should ask for actual rows
with stable inventory-scope and availability ids, dates, net capacity or booked
counts, pricing, and reporting/booking ownership. The legacy static-v1
grouping fields do not turn those pools into AdCP Collections.

### Drafting rows from facts you confirm

Drafting is different from inventing, and the boundary is who states the
sellable facts. When you have uploaded your own media kit or rate card, Murph
may assemble the feed rows for you rather than asking you to produce a CSV:
placement names, formats, and rate-card prices may be seeded from your own
document, but **net sellable capacity, the availability window, and the row
grain must come from you, stated in the conversation** — Murph never estimates
them, and reach or scale claims in a document are never converted into
capacity. Every draft is previewed back to you as normalized rows first, and
committing the feed — putting rows on sale — always requires your explicit
confirmation on a server-enforced control, whether the rows were drafted,
pasted, or uploaded. If you can't state capacity or windows yet, Murph asks
for your actual avails export instead. (Conversational drafting is rolling
out and may not yet be enabled for your account; the confirmation control on
feed commits applies to every account.)

## 1. Check readiness

Start with the runtime projection:

```bash theme={null}
curl "$BASE/inventory-sources/retail-modular/modular" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

The response includes `lifecycleSummary`, one row per stage:

| Status                    | Meaning                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `UNSUPPORTED`             | Every attached module declared what it covers, and none of them implements this stage. Attach a module of the kind that runs it.                                                                                                                                                                                                                                                                              |
| `NOT_DECLARED`            | A module that could run this stage is attached, but we could not read what it does — so whether it runs the stage is unknown, not absent. The stage's `issues` name that module, and `modes` is empty because nothing declared how the stage operates. This is not a setup gap and there is nothing to configure: a module we cannot read is ours to fix. Tell Murph if a stage you depend on reads this way. |
| `MISSING_SETUP`           | A module supports the stage, but required module config is missing.                                                                                                                                                                                                                                                                                                                                           |
| `RUNTIME_INPUTS_REQUIRED` | Setup is complete, but the stage needs runtime input such as an avails feed, creative, or report upload.                                                                                                                                                                                                                                                                                                      |
| `HITL_PENDING`            | A human work item is open for this stage.                                                                                                                                                                                                                                                                                                                                                                     |
| `READY`                   | The stage has enough setup and runtime inputs for the current source state.                                                                                                                                                                                                                                                                                                                                   |
| `BLOCKED`                 | A module has an error or source-health issue that needs operator attention.                                                                                                                                                                                                                                                                                                                                   |

`activeAvailCount` tells you whether the source currently has usable avails. Rows
whose `endTime` has passed do not count as active and are not projected as
sellable products. `openWorkItemCountsByKind` tells you what human queue is
blocking a module or stage, for example `MAP_CREATIVE_REF` or
`UPLOAD_FINAL_REPORT`.

The same response includes `catalogMappingReadiness`, a tenant-scoped read
projection for this exact source. It has three stable sections: **Set up
inventory**, **Make it merchandisable**, and **Prove it**. Each row reports its
requirement, current status, evidence, freshness, source of truth, diagnostics,
external owner when one exists, and one typed next action. `status: null` means
the owning system could not be read, so the row is **Unknown** and never counts
as ready. The top-level `progress` and `nextAction` are the same values Seller
Setup displays; neither surface stores a separate checklist.

Catalog readiness covers the feed and current module contract, Property Roster
authorization and format applicability, components, products, source pricing,
Playbook, AI Business Rules, property-to-decisioning mappings, and source-scoped
brief proof. Booking, creative, trafficking, and reporting stay in the
`lifecycleSummary` pipeline.

## 2. Preview, then commit avails

An inventory-feed module accepts normalized avails. The feed can come from a publisher upload, a pasted CSV/JSON payload, or a file that Murph has parsed into rows. Murph previews parsed rows first; commit the feed only after an operator confirms the normalized rows and confirms `impressionsCapacity` is net sellable capacity.

A preview is row-tolerant: one invalid row does not hide valid rows. The response
returns normalized accepted rows in `avails`, invalid source rows in
`rejectedRows`, and non-blocking normalization facts in `warnings`, with matching
counts. Each diagnostic has a stable `code`, source `rowNumber`, safe `message`,
and `field` when the field is known. `rowNumber` is the one-based data-row
ordinal after excluding the header, so CSV data row 1 is file line 2. For JSON,
it is the one-based position in the `avails` array. Commit by sending only the
accepted `avails` back with `confirmed: true`; rejected rows and warnings are
never committed.

<Warning>
  A commit is an upsert patch keyed by `availId`, not a full-feed replacement.
  Rows omitted from a later file remain unchanged, and a rejected update does not
  retire the last committed version of that row. Resolve every rejected row before
  committing a complete refresh. There is currently no omission-as-delete or
  per-row archive operation: archive the source or let a row expire, and contact
  Apostra when a committed row must be retired early.
</Warning>

Current row diagnostic codes are `REQUIRED_FIELD_MISSING`, `INVALID_DATE_TIME`,
`INVALID_NUMBER`, `INVALID_CURRENCY`, `INVALID_CANONICAL_FORMATS`,
`INVALID_PUBLISHER_PROPERTIES`, `GROSS_CAPACITY_NORMALIZED`, and
`GROSS_CAPACITY_ASSUMED_NET`, plus the `CATALOG_*` Property Roster resolution
codes published by the `static-avails-feed:v1` profile. The two gross-capacity
codes are warnings: preview either
subtracted upstream-booked impressions from gross capacity, or found no booked
input and treated the gross-capacity field as net pending operator confirmation.
Warnings explain normalization only; they never make an otherwise ineligible
row committable.

Formats and Properties are attached to the static-v1 compatibility grouping
rather than one availability window. Supply `formatOptions` and
`publisherProperties` on each row, or send `defaultFormatOptions` and
`defaultPublisherProperties` alongside a JSON preview request. Spreadsheet
cells use JSON-encoded arrays. Every row with the same legacy `collectionId`
field must repeat the same declarations. This attachment does not establish
AdCP Collection identity. The service rejects missing, conflicting, malformed,
or URL-bearing declarations and never infers a format from `channel`. Preview
also resolves each declaration against the current tenant's Property Roster,
but catalog proof is an activation gate rather than an authoring gate. Every
structurally valid row remains in the preview response; `activationReady` and
`activationBlockers` distinguish authored rows from rows eligible to commit.
An unauthorized or unavailable publisher, incomplete roster reconciliation,
missing or ambiguous property identity, absent or mismatched canonical format,
or format outside the selected publisher-property scope blocks commit. The row
is not buyer-visible, reservable, or traffickable while blocked. Commit repeats
the catalog read so a preview cannot bypass later roster drift. Accepted rows
retain their source row number and resolved canonical property and format
identities in server-owned source metadata.

For a specifically approved pilot, Apostra may grant an expiring exception for
one exact `availId` so a paused Storefront can commit that row and run its
seller-owned no-spend lifecycle test. Preview labels this as test-commit ready,
not activation ready. The exception does not authorize the publisher or satisfy
catalog readiness; ordinary buyer access and activation remain blocked until
the publisher-origin catalog resolves.

```bash theme={null}
curl -X POST "$BASE/inventory-sources/retail-modular/modular/avails-feed" \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "confirmed": true,
    "avails": [
      {
        "availId": "sports-week-2026-07",
        "collectionId": "compat-connected-tv-group",
        "collectionName": "Legacy group: Connected TV",
        "formatOptions": [
          {
            "format_option_id": "ctv-vast-30",
            "format_kind": "video_vast",
            "params": { "duration_ms_exact": 30000 }
          }
        ],
        "publisherProperties": [
          {
            "selection_type": "all",
            "publisher_domain": "publisher.example"
          }
        ],
        "name": "Sports week CTV impressions",
        "startTime": "2026-07-01T00:00:00.000Z",
        "endTime": "2026-07-31T23:59:59.000Z",
        "impressionsCapacity": 1500000,
        "cpm": 22,
        "currency": "USD",
        "targeting": {
          "market": "South Africa",
          "screen": "CTV"
        },
        "sourceMetadata": {
          "publisher": "Example Network",
          "upstreamBookedImpressions": 300000
        }
      }
    ]
  }'
```

Ingestion upserts by `availId`. If an avail already has held or booked impressions, the feed update will not reduce capacity below those commitments.

## 3. Inspect product projections

List the product-like projections generated from active avails:

```bash theme={null}
curl "$BASE/inventory-sources/retail-modular/modular/products" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

Each product maps back to one normalized avail and returns the compatibility
grouping's canonical `formatOptions` and `publisherProperties`. A grouping
missing valid declarations fails closed with the typed
`MODULAR_CATALOG_NOT_READY` readiness code before the Merchandising Agent can
use it. The legacy grouping does not become an AdCP Collection. Generic feed-only
sources return `cadent: null`. Cadent fields appear only when the source has an active Cadent
execution module, so uploaded publisher avails are not implicitly treated as
Cadent-backed inventory.

## 4. Reserve capacity

If the source has an active booking-ledger module, hold capacity before finalizing:

```bash theme={null}
curl -X POST "$BASE/inventory-sources/retail-modular/modular/reservations" \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "productId": "avail:sports-week-2026-07",
    "mediaBuyId": "mb_123",
    "buyerCustomerId": 42,
    "requestedImpressions": 250000,
    "holdMinutes": 60
  }'
```

You can pass either `productId` or `availId`. The reservation prevents this modular source from overbooking against capacity it controls. Upstream booked supply must be removed from `impressionsCapacity` before commit; `upstreamBookedImpressions` is explanatory metadata and does not increase sellable capacity.

## 5. Prepare supported execution handoffs

Execution is module-specific. In the current Cadent demo path, the handoff requires an active booking ledger and an active Cadent execution module. It keeps the reservation `HELD`, renders the Cadent campaign XML, and creates source-side work items for trafficking, creative mapping, and reporting. Do not treat the line as upstream-booked until the trafficking step succeeds; completing the `TRAFFIC_TO_CADENT` work item moves the held reservation to `BOOKED`.

```bash theme={null}
curl -X POST "$BASE/inventory-sources/talpa-cadent-demo/modular/bookings/finalize" \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mediaBuyId": "mb_123",
    "availId": "sports-week-2026-07",
    "campaignName": "July CTV sports",
    "advertiserName": "Example Advertiser",
    "creative": {
      "format": "video",
      "durationSeconds": 30,
      "assetUrl": "https://assets.example.com/creative.mp4"
    }
  }'
```

For a source that books through a person rather than an execution API, create
the source with `execution.mode: "manual"`. Apostra attaches the
provider-neutral manual execution module. Finalizing a held booking then opens
three seller work items: `EXECUTE_UPSTREAM_BOOKING`,
`SYNC_UPSTREAM_CREATIVE`, and `UPLOAD_FINAL_REPORT`. Completing them records
the upstream campaign/order/placement ids, creative references, and final
delivery evidence on the booking; the first completion moves capacity from
`HELD` to `BOOKED`, and the final report moves it to `DELIVERED`.

## 6. Release capacity

Release a held reservation when a buyer cancels, a hold expires, or the execution handoff fails:

```bash theme={null}
curl -X POST "$BASE/inventory-sources/retail-modular/modular/bookings/release" \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mediaBuyId": "mb_123",
    "availId": "sports-week-2026-07"
  }'
```

Downgrading to Basic does not cancel or delete modular-source lifecycle state.
Existing `HELD` reservations continue to reduce
available capacity until they expire or are released through this operation;
`BOOKED` reservations remain booked. Open source-side work items and pending
reports keep their current status and history. The downgrade prevents new
composition and hides pilot surfaces, but it does not represent upstream
cancellation, completed human work, or completed reporting.

## 7. Work the human queue

Some stages are intentionally human-in-the-loop. Common examples:

| Work item kind          | When it appears                                                                                               |
| ----------------------- | ------------------------------------------------------------------------------------------------------------- |
| `CONFIRM_AVAILS`        | An operator must confirm or correct incoming avails before products are shown.                                |
| `MAP_CREATIVE_REF`      | A buyer creative needs a broadcast-system creative ID or trafficking reference.                               |
| `TRAFFIC_TO_CADENT`     | A Cadent campaign payload is ready for a human operator to traffic; completing it books the held reservation. |
| `UPLOAD_FINAL_REPORT`   | The campaign has ended and delivery reporting needs a file upload.                                            |
| `RESOLVE_SOURCE_HEALTH` | A module is blocked or misconfigured.                                                                         |

These work items belong to the inventory source, not the buyer-facing storefront task. The storefront can still expose buyer task status or webhooks while source operators complete the underlying queue.

`RESOLVE_SOURCE_HEALTH` tracks one continuous outage, not one item per failed
check: it opens when the source first goes unhealthy and closes automatically
once the source recovers. Repeated checks update the source's health diagnostics
while the same item remains open instead of raising duplicates. If Apostra later
determines the outage is on our side rather than yours, the item is withdrawn
rather than left open against you.

## Demo profiles and legacy source recipes

The canonical product demo is [Demo Storefront](/v2/storefront/inventory-sources/demo-storefront).
Its versioned, code-owned profiles compose existing modular contracts and are
clearly labeled synthetic. They do not call or claim to emulate a vendor API.

`POST /inventory-sources/modular/cadent-demo` creates or resets a demo-only source with static avails, a booking ledger, and Cadent execution. Use it to test the end-to-end lifecycle, not as a publisher avails ingestion path. Demo source IDs must be `talpa-cadent-demo` or start with `demo-`.

That Cadent-shaped recipe and the FreeWheel sandbox route remain useful
integration-development precedents. They are not the backing data plane for
Demo Storefront and do not turn a vendor sandbox into the canonical demo.

## Related tasks

<CardGroup cols={2}>
  <Card title="Prepare inventory source inputs" href="/v2/setup/publisher-onboarding-starter-kit" icon="clipboard-check">
    Evidence checklist, templates, recovery steps, and readiness milestones
  </Card>

  <Card title="Get modular readiness" href="/v2/storefront/inventory-sources/tasks/get-modular-readiness" icon="diagram-project">
    Inspect lifecycle status, module contracts, setup gaps, and work-item counts
  </Card>

  <Card title="Update module config" href="/v2/storefront/inventory-sources/tasks/update-modular-module-config" icon="sliders">
    Write non-secret module setup fields
  </Card>
</CardGroup>
