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

# Modular sources

> Sell inventory from an avails spreadsheet or export: create a modular source, check its readiness, then preview and commit static-avails-feed:v1 rows that become products.

A **modular source** is an inventory source assembled from modules instead of one upstream sales
agent. Use it when what you can sell lives in a spreadsheet or an export, and booking, trafficking
and reporting happen in other systems or by hand. Buyers still see one storefront and buy through
its usual media-buy flow.

Every modular source starts with two built-in modules: an **avails feed** (the `static-avails-feed:v1`
profile) and a **booking ledger**. By default it also gets a provider-neutral **manual execution**
module. Nothing is offered to buyers until you commit avails rows.

<Note>
  Modular sources need a **Merchandising** plan (`MERCHANDISING`, `MERCHANDISING_DISTRIBUTION` or
  `ENTERPRISE_MERCHANDISING`). On any **Listing** plan, or with no plan, the **Modular source** choice
  is unavailable and creating one is refused with "Custom modular sources are included with the
  Merchandising profile. Your plan includes Listing only." A
  source you already have keeps working after a downgrade: you can still read its readiness and
  commit feeds.
</Note>

## The three surfaces

| Surface                      | What it's for                                                                                 | `open_page` name                                             |
| ---------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| **Add a modular source**     | Register a source: its name, source ID, where its avails come from and how often they arrive. | `modular_source_setup`                                       |
| **Modular inventory source** | The workspace for one source: lifecycle stages, catalog readiness, modules and products.      | `modular_inventory_source` (`sourceId`)                      |
| **Import inventory feed**    | Preview an avails file or pasted rows, then commit the rows that are ready.                   | `modular_inventory_feed` (`sourceId`, optional `sourceName`) |

Open them from **Seller setup → Inventory sources → Add a source → Modular source**, from the source's
**Open workspace** button, from the rail's inventory source list, or by asking Semi ("Add a modular
source."). From an agent client, call `open_page` with the page name; pass the page's context in
`arguments` (`{"page": "modular_inventory_source", "arguments": {"sourceId": "sample-ctv"}}`).

Creating a source doesn't ask how you book, traffic, clear creative or report: the workspace derives
that per lifecycle stage from the modules attached. The completion operations behind the pages
(`create_feed_modular_inventory_source`, `preview_inventory_feed`, `commit_inventory_feed`,
`get_inventory_feed_template`) are page-only: the page calls them after you confirm, and feed files
stay in the page, so the model never receives the bytes.

## Create a source

`POST /api/v2/storefront/inventory-sources/modular/feed` (admins only):

```bash theme={null}
curl -X POST "https://api.semicola.com/api/v2/storefront/inventory-sources/modular/feed" \
  -H "Authorization: Bearer $SEMICOLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sourceId": "sample-ctv",
    "name": "Sample CTV avails",
    "availsSource": { "type": "spreadsheet", "cadence": "monthly" },
    "execution": { "mode": "manual" }
  }'
```

| Field                            | Notes                                                                                                                                              |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                           | Required, 1–255 characters.                                                                                                                        |
| `sourceId`                       | Optional storefront-scoped ID: lowercase letters, numbers, dashes or underscores, up to 64. Defaults to a slug of `name`. Can't start with `src_`. |
| `availsSource`                   | Optional `type` (`upload`, `spreadsheet`, `api`, `manual` or `unknown`), `cadence` and `description`.                                              |
| `booking`                        | Optional `mode` (`booking_ledger`, `manual` or `upstream_oms`), `systemName`, `notes`.                                                             |
| `execution`                      | Optional `mode` (`manual`, `upstream_system` or `none`, default `manual`) and `systemName`.                                                        |
| `includeBookingLedger`           | Default `true`. `false` needs an explicit non-manual `execution.mode`.                                                                             |
| `reporting`, `creative`, `notes` | Optional, non-secret setup notes kept on the source.                                                                                               |

Never send secrets here. The response is `201` with `inventorySourceId`, `sourceId` and the source's
readiness `projection`. With `execution.mode` `upstream_system` or `none`, no execution module is
attached and the response adds a `NO_EXECUTION_MODULE` warning ("This source cannot finalize buyer
bookings until a compatible trafficking module is attached."). A `sourceId` already used on your
storefront is `409`; a malformed one is `400`.

## Check readiness

`GET /api/v2/storefront/inventory-sources/{sourceId}/modular` returns the runtime projection (the
**Modular inventory source** page shows the same data). `{sourceId}` is the storefront-scoped ID or
Semicola's internal source id; an unknown one is `404`.

* `modules[]`: each module's `kind` (`INVENTORY_FEED`, `BOOKING_LEDGER`, `TRAFFICKING`, …), `status`,
  lifecycle stages with their `mode` (`AUTOMATED`, `HITL` or `UNSUPPORTED`) and missing setup fields.
* `lifecycleSummary[]`: one row per stage for `INGEST_AVAILS`, `GET_PRODUCTS`, `RESERVE_AVAILS`,
  `RELEASE_BOOKING`, `FINALIZE_BOOKING`, `SYNC_CREATIVES` and `IMPORT_REPORTING`.
* `activeAvailCount`: committed rows whose `endTime` hasn't passed. `openWorkItemCount`: the
  source's open work items (each module also reports `openWorkItemCountsByKind`).
* `catalogMappingReadiness`: three sections (**Set up inventory**, **Make it merchandisable**,
  **Prove it**), each row with its requirement, status, evidence and one next action, plus
  `progress`, `requiredProgress` and `nextAction` (`null` once every required row is ready). It
  checks your committed rows, the Property Roster, products, CPMs, your
  [Playbook](/sell/playbook) and [AI Business Rules](/sell/ai-business-rules).

| Stage status              | Meaning                                                                                            |
| ------------------------- | -------------------------------------------------------------------------------------------------- |
| `READY`                   | The stage has what it needs.                                                                       |
| `RUNTIME_INPUTS_REQUIRED` | Set up, but waiting on input: for the feed stages, committed avails.                               |
| `NOT_DECLARED`            | A module that could run the stage is attached, but Semicola doesn't run the stage yet (see below). |
| `UNSUPPORTED`             | No attached module runs the stage.                                                                 |
| `BLOCKED`                 | The module is disabled or has an error.                                                            |

`RESERVE_AVAILS` and `RELEASE_BOOKING` are `READY` once active avails exist. With the manual
execution module, `FINALIZE_BOOKING` and `SYNC_CREATIVES` are person-run (`HITL`) stages: `READY`,
or `HITL_PENDING` while a work item of their kind is open. `IMPORT_REPORTING` stays `NOT_DECLARED`:
a final-report work item records delivered impressions on the booking, but nothing imports them
into delivery reporting yet.

## Products

Each committed row becomes one product, `avail:<availId>`, priced by CPM, with the row's formats,
properties and channel. `GET /api/v2/storefront/inventory-sources/{sourceId}/modular/products` lists
them with capacity (`impressionsCapacity`, `heldImpressions`, `bookedImpressions`,
`availableImpressions`), `cadent: null` and
`active`. Once a row's `endTime` passes it isn't sold: discovery skips it and a media buy for it is
refused. The seller setup source card reads "Healthy · N active avails" or "Awaiting avails".

## Book capacity

The booking ledger holds capacity against one avail without overbooking.

* `POST …/modular/reservations` takes `productId` or `availId`, `mediaBuyId`, `packageId`,
  `buyerCustomerId`, `requestedImpressions` and an optional `holdMinutes`. It's idempotent per
  source, avail, media buy and package. Overbooking is `409` with `details.availableImpressions`; an
  ended avail is `409`; a source without an active ledger module is `422`.
* `POST …/modular/bookings/release` releases a held booking or cancels a booked one, returning the
  capacity to the avail, and cancels the booking's open work items.
* A media buy on a modular product holds its capacity at intake (budget ÷ CPM × 1,000), and one that
  would overbook is refused. A buyer cancel or your rejection releases it. A feed commit never takes
  an avail's capacity below what's held or booked.

## Finalize and work items

With the manual execution module (the default), `POST …/modular/bookings/finalize` prepares a held
booking for execution: it opens three work items for your team, **Book the order in your system**
(`EXECUTE_UPSTREAM_BOOKING`), **Send the creative to your system** (`SYNC_UPSTREAM_CREATIVE`) and
**Upload the final delivery report** (`UPLOAD_FINAL_REPORT`). The booking stays `HELD` and the
response says `PENDING_TRAFFICKING`. When you accept a buy, its held bookings are finalized for you.

* Completing the first work item moves the booking to `BOOKED`; the final report moves it to
  `DELIVERED`. Each completion needs its required result (`upstreamOrderId`, `creativeRefs`,
  `deliveredImpressions`) and records it on the booking.
* `GET …/modular/work-items` lists them (by default `OPEN`, `IN_PROGRESS` and `BLOCKED`);
  `GET …/work-items/{workItemId}` adds the event history; `PATCH` changes status, assignee, blocked
  reason or notes, but can't complete; `POST …/work-items/{workItemId}/complete` completes one.
  Completing twice returns `changed: false`.
* The **Work items** section of the Modular inventory source page lists them and completes them.
  Pending Operations groups them by media buy under manual source work.

### The work queue in chat

`search({ "kind": "work_item" })` reads every task waiting on you: modular source follow-ups, media-buy
approvals and creative reviews. Each result's `workItemKind` (`modular_source`, `media_buy_approval`,
`creative_review`) sets its status vocabulary; follow-ups also carry `requiredResultFields`, and
creative reviews a `contentDigest`. The response's `coverage` says which queues were searched and how
many modular sources the scan reached. A status one queue can't have leaves that queue out and says
so. To read one source's queue, pass `filter.sourceId` with `filter.workItemKind: "modular_source"`
(a `sourceId` alone is refused).

`save_work_item` completes or updates one item: a follow-up with `status: "COMPLETED"` and its
result, a media-buy approval or creative review with `approved` or `rejected` (a creative review also
needs `expectedContentDigest`). Saving the same result again returns `"action": "unchanged"`; a
conflicting correction is refused and the original record stays. Re-recording an approval never
re-sends a forward.

## Feed profiles (REST bulk transport)

Integrations send avails over REST instead of the Import inventory feed Task.

1. Register the feed: `POST …/inventory-sources/{sourceId}/feeds` (or `create_inventory_feed`) with
   `profileId: "static-avails-feed"` and `profileVersion: "v1"`. Registration is idempotent, the
   canonical key is `static-avails-feed:v1`, and no credential is issued: use your account API key.
   `v1-push` is accepted when the source's avails-feed module declares `availsSource.type: "api"`.
2. `POST …/feed/preview` (JSON `rawRows`, `csvText` or `jsonText`) or `POST …/feed/upload`
   (multipart file) returns a `PREVIEW` revision (facts, diagnostics, `validationStatus`) and a
   signed `previewBaseline`.
3. `POST …/feed/commit` with the `feedId`, `previewBaseline` and `revision` as returned. If another
   commit moved the feed since the preview, it's `422 STALE_PREVIEW_BASELINE`; preview again.
4. With `v1-push`, `POST …/feed/push` parses, validates and commits in one call (`dryRun: true`
   validates only).

`GET …/feeds` and `GET …/feeds/{feedId}` (or `get_inventory_feed_status`) report the head, its
transport, sync health and the latest attempt as counts, digests and diagnostic codes, never rows.
Pull, schedules, provider triggers, feed credentials and signed transfers are reported as
`UNSUPPORTED_OPERATION`.

## Preview discovery for one source

When **Get products** is **Ready**, select **Open read-only discovery preview** in the source's
workspace, enter a representative buyer brief and select **Run read-only preview**. The Task shows
only the products that source would return through your storefront. Each run rechecks that the
source is enabled and Get products is still Ready, and stops otherwise. The preview is recorded for
later review; it doesn't create a media buy, change live products or source setup, or turn on
transacting.

## Not available yet

* The `wholesale-avails-pricing` feed profile, module configuration or credentials, importing
  final reports into delivery reporting, and failed-order cleanups for ad-server sources.
* A no-spend source test campaign, and Semi drafting rows from an uploaded document.
* Deleting or archiving a single committed row.

## Related

* [Inventory sources](/sell/inventory-sources)
* [Products](/sell/products)
* [Publisher example pack](/sell/publisher-example-pack)
