Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ All notable changes to the IAB Tech Lab Seller Agent are documented here.

### Changed

- Index Exchange SSP connector rewritten against the real `/v3/deals` API
(create, get, list, update, clone, troubleshoot, seat-ID to DSP
resolution). Seat IDs that resolve to several DSPs pause for operator
approval instead of guessing; approvals, their listing and resume are
operator-only, and a resumed approval is consumed so it cannot create the
deal twice. `INDEX_EXCHANGE_API_URL` has no default, so an unconfigured
deployment never calls production.
- Booking (POST /api/v1/deals) now requires a verified buyer key matching
the quote; the MCP deal-from-template tool is operator-gated. Previously
anonymous callers could book any quote.
Expand Down
15 changes: 11 additions & 4 deletions docs/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2875,7 +2875,7 @@
"Approvals"
],
"summary": "List Pending Approvals",
"description": "List all pending approval requests. Requires authentication.",
"description": "List all pending approval requests. Requires an operator credential.",
"operationId": "list_pending_approvals_approvals_get",
"parameters": [
{
Expand Down Expand Up @@ -2939,7 +2939,7 @@
"Approvals"
],
"summary": "Get Approval",
"description": "Get a specific approval request and its response (if any).\n\nRequires authentication.",
"description": "Get a specific approval request and its response (if any).\n\nRequires an operator credential.",
"operationId": "get_approval_approvals__approval_id__get",
"parameters": [
{
Expand Down Expand Up @@ -3012,7 +3012,7 @@
"Approvals"
],
"summary": "Decide Approval",
"description": "Submit a human decision for a pending approval. Requires authentication.\n\nThe verified principal from the authenticated API key is stamped into\nthe audit record; ``body.decided_by`` is kept as a display label only.",
"description": "Submit a human decision for a pending approval. Requires an operator credential.\n\nThe verified principal from the authenticated API key is stamped into\nthe audit record; ``body.decided_by`` is kept as a display label only.",
"operationId": "decide_approval_approvals__approval_id__decide_post",
"parameters": [
{
Expand Down Expand Up @@ -3095,7 +3095,7 @@
"Approvals"
],
"summary": "Resume Flow",
"description": "Resume a flow after an approval decision has been submitted.\n\nRequires authentication. Loads the flow state snapshot, applies the\ndecision, and returns the final result without re-running expensive\ncrew evaluations.",
"description": "Resume a flow after an approval decision has been submitted.\n\nRequires an operator credential. Loads the flow state snapshot, applies the\ndecision, and returns the final result without re-running expensive\ncrew evaluations.",
"operationId": "resume_flow_approvals__approval_id__resume_post",
"parameters": [
{
Expand Down Expand Up @@ -10424,6 +10424,13 @@
"additionalProperties": true,
"type": "object"
},
{
"items": {
"additionalProperties": true,
"type": "object"
},
"type": "array"
},
{
"type": "null"
}
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ ad-seller freewheel-login --provider bc
| `SSP_ROUTING_RULES` | `str` | `""` | Routing rules: `ctv:pubmatic,display:index_exchange` |
| `PUBMATIC_MCP_URL` | `str` | `None` | PubMatic MCP endpoint (e.g., `https://mcp.pubmatic.com/sses`) |
| `PUBMATIC_API_KEY` | `str` | `None` | PubMatic API key |
| `INDEX_EXCHANGE_API_URL` | `str` | `None` | Index Exchange REST API URL |
| `INDEX_EXCHANGE_API_KEY` | `str` | `None` | Index Exchange API key |
| `INDEX_EXCHANGE_API_URL` | `str` | `None` | Index Exchange REST API FQDN only (no path), e.g. `https://app.indexexchange.com` — the connector applies path prefixes per-endpoint |
| `INDEX_EXCHANGE_API_KEY` | `str` | `None` | Keycloak JWT bearer token (not a static API key — expires and must be refreshed) |
| `MAGNITE_API_URL` | `str` | `None` | Magnite REST API URL |
| `MAGNITE_API_KEY` | `str` | `None` | Magnite API key |

Expand Down
6 changes: 3 additions & 3 deletions docs/guides/developer-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ SSP_CONNECTORS=pubmatic,index_exchange
PUBMATIC_MCP_URL=https://mcp.pubmatic.com/sses
PUBMATIC_API_KEY=your-pubmatic-key

# Index Exchange (REST)
INDEX_EXCHANGE_API_URL=https://api.indexexchange.com
INDEX_EXCHANGE_API_KEY=your-ix-key
# Index Exchange (REST) — FQDN only, no path; the connector applies /api/deals itself
INDEX_EXCHANGE_API_URL=https://app.indexexchange.com
INDEX_EXCHANGE_API_KEY=your-keycloak-jwt-bearer-token

# Routing rules (optional)
SSP_ROUTING_RULES=ctv:pubmatic,display:index_exchange
Expand Down
31 changes: 26 additions & 5 deletions docs/guides/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,19 +110,40 @@ The deal was created in the seller agent but not yet pushed to PubMatic.

### Index Exchange API Errors

**Error: `Index Exchange: 401 Unauthorized`**

`INDEX_EXCHANGE_API_KEY` must be a valid Keycloak JWT bearer token, not a
static API key — it expires and must be refreshed via the client-credentials
grant. This is not automated yet; re-issue a fresh token in your `.env`.

**Error: `Index Exchange: 403 Forbidden`**

Your API key doesn't have permission for that operation.
Your token doesn't have permission for that operation.

1. Verify `INDEX_EXCHANGE_API_KEY` in your `.env` is current (not expired).
2. Contact your Index Exchange account manager to confirm the token has deal management scope.

**Error: `ValueError: external_deal_id is required` / `account_id is required` / `dsp_id is required`**

1. Verify `INDEX_EXCHANGE_API_KEY` in your `.env`.
2. Contact your Index Exchange account manager to confirm the key has deal management scope.
`/v3/deals` requires `externalDealID`, `account.accountID`, and `dspID`
(`directConfigurations.dspID` for Direct deal types, or
`marketplaceConfigurations.dspID` for Marketplace Package) on every create
call. `external_deal_id` is supplied automatically by the seller-agent's
deal flow. `dsp_id` can be resolved automatically from `buyer_seat_ids` (set
at deal-creation time, e.g. via `create_curated_deal`'s `buyer_seat_ids`
param) — if the seat ID(s) match more than one DSP, the deal is held
pending human approval instead of guessing (see `approve_or_reject` /
`resume_approval`). `account_id` has no automatic source yet; pass it
explicitly via `distribute_deal_via_ssp`'s `account_id` param if it isn't
already on the stored deal.

**Error: `Index Exchange: 422 Unprocessable Entity`**

Deal data failed validation on the Index Exchange side.

- Check the response body for a `errors` field — it will name the invalid field.
- Common causes: floor price below IX minimum, unsupported ad format, missing `seller_id`.
- Check the response body for an `errors` field — it will name the invalid field.
- Common causes: `floor` below IX minimum, `externalDealID` starting with `0`
or outside 3–64 chars, missing `directConfigurations.dspID` on a Direct deal.

---

Expand Down
3 changes: 2 additions & 1 deletion docs/reference/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Tools exposed by the seller MCP server (`ad_seller.interfaces.mcp_server`),
enumerated from the live FastMCP registry.

**Total: 47 tools**
**Total: 48 tools**

| Tool | Description |
| --- | --- |
Expand Down Expand Up @@ -49,6 +49,7 @@ enumerated from the live FastMCP registry.
| `push_deal_to_buyers` | Push a deal to buyer endpoints via IAB Deals API v1.0. |
| `register_buyer_agent` | Discover and register a buyer agent by URL. |
| `request_quote` | Request a non-binding price quote for a product. |
| `resume_approval` | Resume a flow that was paused for approval, after a decision has been |
| `revoke_api_key` | Revoke an API key. |
| `set_agent_trust` | Set trust level for a buyer agent. |
| `set_approval_gates` | Configure approval gates. required_flows is comma-separated: |
Expand Down
17 changes: 15 additions & 2 deletions src/ad_seller/clients/ssp_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@

from pydantic import BaseModel


class SSPDealOutcomeUnknownError(Exception):
"""create_deal may or may not have created the deal and this could not be confirmed."""


# =============================================================================
# SSP result models
# =============================================================================
Expand Down Expand Up @@ -76,7 +81,9 @@ class SSPDeal(BaseModel):
priority: Optional[str] = None # SSP-specific (e.g., PubMatic P11-P15)
start_date: Optional[str] = None
end_date: Optional[str] = None
targeting: Optional[dict[str, Any]] = None
# Shape varies by SSP: a flat dict (e.g. PubMatic) or a structured array of
# targeting objects (e.g. Index Exchange's {targetingType, keyName, sets}).
targeting: Optional[Any] = None
impressions_goal: Optional[int] = None
ssp_type: SSPType = SSPType.CUSTOM
ssp_name: Optional[str] = None # Human-readable SSP name
Expand Down Expand Up @@ -107,11 +114,17 @@ class SSPDealCreateRequest(BaseModel):
priority: Optional[str] = None
start_date: Optional[str] = None
end_date: Optional[str] = None
targeting: Optional[dict[str, Any]] = None
# Shape varies by SSP: a flat dict (e.g. PubMatic) or a structured array of
# targeting objects (e.g. Index Exchange's {targetingType, keyName, sets}).
targeting: Optional[dict[str, Any] | list[dict[str, Any]]] = None
impressions_goal: Optional[int] = None
buyer_seat_ids: list[str] = []
# For clone operations
source_deal_id: Optional[str] = None
# SSP-assigned identifiers some SSPs (e.g. Index Exchange) require at creation time
external_deal_id: Optional[str] = None
account_id: Optional[int] = None
dsp_id: Optional[int] = None


# =============================================================================
Expand Down
Loading
Loading