openapi: 3.2.0 info: title: Ad Seller System Negotiation API description: IAB OpenDirect 2.1 compliant seller agent for programmatic advertising. Supports product discovery, tiered pricing, proposal evaluation, multi-round negotiation, deal execution, order management, and change requests. contact: name: IAB Tech Lab url: https://iabtechlab.com/ license: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0 version: 1.0.0 tags: - name: Negotiation description: Multi-round price negotiation paths: /proposals/{proposal_id}/counter: post: tags: - Negotiation summary: Counter Proposal description: 'Submit a counter-offer in an ongoing negotiation. Loads or creates a NegotiationHistory, evaluates the buyer''s offer, persists the updated history, and emits a NEGOTIATION_ROUND event. EP-5.2: this endpoint previously applied NO trust-tier ceiling — a buyer could self-assert ADVERTISER pricing by populating advertiser_id. The claimed tier is now verified against the agent registry (agent_url) or the API key; unverifiable claims floor.' operationId: counter_proposal_proposals__proposal_id__counter_post parameters: - name: proposal_id in: path required: true schema: type: string title: Proposal Id - name: authorization in: header required: false schema: anyOf: - type: string - type: 'null' title: Authorization - name: X-Api-Key in: header required: false schema: anyOf: - type: string - type: 'null' title: X-Api-Key requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CounterOfferRequest' responses: '200': description: Successful Response content: application/json: schema: {} '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /proposals/{proposal_id}/negotiation: get: tags: - Negotiation summary: Get Negotiation Status description: Get full negotiation history for a proposal. operationId: get_negotiation_status_proposals__proposal_id__negotiation_get parameters: - name: proposal_id in: path required: true schema: type: string title: Proposal Id responses: '200': description: Successful Response content: application/json: schema: {} '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' /api/v1/negotiations/messages: post: tags: - Negotiation summary: Post Negotiation Message description: 'Canonical negotiation surface — accepts the shared NegotiationMessage. The seller now validates the SAME message the buyer emits: a required ``action`` enum (accept/counter/reject/final_offer) plus ``buyer_price`` as :class:`Money`. A well-formed counter no longer 422s (the historical bug). Priced actions (counter/final_offer) run the seller''s untouched negotiation engine; accept/reject are recorded as terminal rounds off the existing history.' operationId: post_negotiation_message_api_v1_negotiations_messages_post parameters: - name: authorization in: header required: false schema: anyOf: - type: string - type: 'null' title: Authorization - name: X-Api-Key in: header required: false schema: anyOf: - type: string - type: 'null' title: X-Api-Key requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/NegotiationMessage' responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/NegotiationRoundResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' components: schemas: ValidationError: properties: loc: items: anyOf: - type: string - type: integer type: array title: Location msg: type: string title: Message type: type: string title: Error Type input: title: Input ctx: type: object title: Context type: object required: - loc - msg - type title: ValidationError NegotiationMessage: properties: idempotency_key: type: string minLength: 1 title: Idempotency Key description: Requester-minted opaque key (UUID recommended). Same key -> same response, no duplicate side effects (FD-12). Reusing a key with a different body is an idempotency_conflict error. action: $ref: '#/components/schemas/NegotiationAction' description: 'REQUIRED move discriminator: ''accept'', ''counter'', ''reject'' (walk-away), or ''final_offer''. No default — the action-less bare-price payload that caused the historical 422 does not validate.' negotiation_id: anyOf: - type: string - type: 'null' title: Negotiation Id description: Seller-issued id of the negotiation to continue; None opens a new negotiation on proposal_id/quote_id. proposal_id: anyOf: - type: string - type: 'null' title: Proposal Id description: Proposal under negotiation, if proposal-led. quote_id: anyOf: - type: string - type: 'null' title: Quote Id description: Quote under negotiation, if quote-led. round_number: anyOf: - type: integer minimum: 1.0 - type: 'null' title: Round Number description: The round the sender believes it is answering, for optimistic concurrency; a mismatch is a 'contention' error. The seller's numbering is authoritative. buyer_price: anyOf: - $ref: '#/components/schemas/Money' - type: 'null' description: The buyer's price this round (exact micros; FD-11). REQUIRED for 'counter'/'final_offer'; optional echo on 'accept'; omitted on 'reject'. buyer_identity: anyOf: - $ref: '#/components/schemas/BuyerIdentity' - type: 'null' rationale: type: string title: Rationale description: Optional human-readable rationale. default: '' type: object required: - idempotency_key - action title: NegotiationMessage description: 'A buyer negotiation move (money-mutating: FD-12). Exactly one negotiation context is required: ``negotiation_id`` to continue, or ``proposal_id``/``quote_id`` to open (the seller mints ``negotiation_id`` on open).' NegotiationRound: properties: round_number: type: integer minimum: 1.0 title: Round Number buyer_price: $ref: '#/components/schemas/Money' description: What the buyer offered this round. seller_price: $ref: '#/components/schemas/Money' description: What the seller countered (or accepted at) this round. action: $ref: '#/components/schemas/NegotiationAction' concession_pct: type: number title: Concession Pct description: Seller concession this round (0-1). default: 0.0 cumulative_concession_pct: type: number title: Cumulative Concession Pct description: Total seller concession so far (0-1). default: 0.0 rationale: type: string title: Rationale default: '' timestamp: type: string format: date-time title: Timestamp type: object required: - round_number - buyer_price - seller_price - action title: NegotiationRound description: A single offer/counter round in a negotiation. Money: properties: amount_micros: type: integer title: Amount Micros description: Amount in micros; 1,000,000 micros = 1 currency unit. currency: type: string pattern: ^[A-Z]{3}$ title: Currency description: ISO 4217 alpha-3 currency code. default: USD type: object required: - amount_micros title: Money description: 'Exact money amount in integer micros (flagged decision FD-11). ``1_000_000`` micros = 1 currency unit — the ad-industry convention (Google Ad Manager, among others, prices in micros). Float is BANNED on the wire for money: IEEE 754 floating point is non-deterministic for money math (``0.1 + 0.2 != 0.3``, and repeated CPM — cost per mille — arithmetic accumulates error), and the two source repos used ``float`` end-to-end; that defect must not be fossilized into the spec. Every price, rate, budget, and offer in the shared contract is a ``Money``. ``amount_micros`` is a strict integer: float inputs are rejected at validation time rather than silently truncated.' NegotiationRoundResponse: properties: negotiation_id: type: string title: Negotiation Id description: Seller-issued negotiation identifier. status: $ref: '#/components/schemas/NegotiationStatus' description: '''active'' or terminal (''accepted''/''rejected''/''expired''); terminal negotiations refuse further messages (negotiation_closed).' round: $ref: '#/components/schemas/NegotiationRound' rounds_remaining: anyOf: - type: integer minimum: 0.0 - type: 'null' title: Rounds Remaining description: Rounds left before the seller walks away, if disclosed. type: object required: - negotiation_id - status - round title: NegotiationRoundResponse description: 'The seller''s answer to a :class:`NegotiationMessage`. Embeds the shared :class:`~iab_agentic_primitives.primitives.NegotiationRound` primitive — the same record appended to the Negotiation history — so the two sides cannot disagree about what a round contains. ``round.action`` is the SELLER''s move (accept/counter/reject/final_offer); when ``status`` is terminal the round is the last one.' NegotiationAction: type: string enum: - accept - counter - reject - final_offer title: NegotiationAction description: Action taken in a negotiation round. BuyerIdentity: properties: seat_id: anyOf: - type: string - type: 'null' title: Seat Id description: DSP seat identifier. seat_name: anyOf: - type: string - type: 'null' title: Seat Name description: DSP platform display name. dsp_platform: anyOf: - type: string - type: 'null' title: Dsp Platform description: DSP platform slug (e.g. 'ttd', 'dv360'). agency_id: anyOf: - type: string - type: 'null' title: Agency Id agency_name: anyOf: - type: string - type: 'null' title: Agency Name agency_holding_company: anyOf: - type: string - type: 'null' title: Agency Holding Company advertiser_id: anyOf: - type: string - type: 'null' title: Advertiser Id advertiser_name: anyOf: - type: string - type: 'null' title: Advertiser Name advertiser_industry: anyOf: - type: string - type: 'null' title: Advertiser Industry campaign_id: anyOf: - type: string - type: 'null' title: Campaign Id description: Optional campaign scope for campaign-specific deals. campaign_name: anyOf: - type: string - type: 'null' title: Campaign Name type: object title: BuyerIdentity description: 'Buyer identity revealed progressively to unlock better pricing. Superset of the buyer repo''s ``BuyerIdentity`` and the seller repo''s ``BuyerIdentity``/``QuoteBuyerIdentity``; every field is optional so a partially revealed identity is always representable. The tier derived from these fields is capped server-side by the registry-verified trust status of the calling agent — it is never self-asserted.' CounterOfferRequest: properties: buyer_price: type: number title: Buyer Price buyer_tier: type: string title: Buyer Tier default: public agency_id: anyOf: - type: string - type: 'null' title: Agency Id advertiser_id: anyOf: - type: string - type: 'null' title: Advertiser Id agent_url: anyOf: - type: string - type: 'null' title: Agent Url type: object required: - buyer_price title: CounterOfferRequest description: Request to submit a counter-offer in a negotiation. HTTPValidationError: properties: detail: items: $ref: '#/components/schemas/ValidationError' type: array title: Detail type: object title: HTTPValidationError NegotiationStatus: type: string enum: - active - accepted - rejected - expired title: NegotiationStatus description: Status of a negotiation (typed; was a raw string in the seller repo).