Deal Booking¶
This guide walks you through the complete deal booking process --- from browsing a seller's inventory to getting a confirmed Deal ID you can activate in your DSP.
Prerequisites¶
Before you start, you need:
- A running seller agent --- The seller's API must be reachable (e.g.
http://seller.example.com:8001). See Seller Agent Integration for connection details. - An API key --- Get one from the seller for authenticated access. Public browsing works without a key, but you need one to see exact pricing and book deals.
- Python 3.11+ with the
ad_buyerpackage installed.
Check your access tier
Your API key determines your access tier (PUBLIC, SEAT, AGENCY, ADVERTISER). Higher tiers unlock better pricing and negotiation rights. See Authentication for details.
Quick Example¶
Here's the minimum code to go from zero to a booked deal:
import asyncio
from ad_buyer.clients.deals_client import DealsClient
from ad_buyer.models.deals import QuoteRequest, DealBookingRequest
async def book_a_deal():
async with DealsClient(
seller_url="http://seller.example.com:8001",
api_key="your-api-key",
) as client:
# 1. Request a quote
quote = await client.request_quote(QuoteRequest(
product_id="prod-premium-video",
deal_type="PD",
impressions=500_000,
))
print(f"Quote: ${quote.pricing.final_cpm} CPM (expires: {quote.expires_at})")
# 2. Book it
deal = await client.book_deal(DealBookingRequest(
quote_id=quote.quote_id,
))
print(f"Deal ID: {deal.deal_id}")
print(f"[Open Real-Time Bidding (OpenRTB)](https://iabtechlab.com/standards/openrtb/) floor: ${deal.openrtb_params.bidfloor}")
asyncio.run(book_a_deal())
That's the core loop: quote, then book. The rest of this guide covers each step in detail, plus negotiation, persistence, and the higher-level flows.
Step-by-Step Walkthrough¶
Step 1: Authenticate with the Seller¶
Get an API key from the seller. The key maps to an access tier on the seller side:
| Tier | What You Reveal | What You Get |
|---|---|---|
| PUBLIC | Nothing | Price ranges only |
| SEAT | DSP seat ID | ~5% discount, exact pricing |
| AGENCY | Agency ID | ~10% discount, negotiation rights |
| ADVERTISER | Advertiser ID | ~15% discount, negotiation rights |
Pass your key when creating the client:
from ad_buyer.clients.deals_client import DealsClient
client = DealsClient(
seller_url="http://seller.example.com:8001",
api_key="your-api-key",
)
For bearer token auth instead:
client = DealsClient(
seller_url="http://seller.example.com:8001",
bearer_token="your-bearer-token",
)
Step 2: Browse Inventory (Media Kit)¶
Before requesting a quote, find what the seller has available. The MediaKitClient lets you browse the seller's catalog:
from ad_buyer.media_kit import MediaKitClient
async with MediaKitClient(api_key="your-api-key") as mk:
# Get the full catalog
kit = await mk.get_media_kit("http://seller.example.com:8001")
print(f"{kit.seller_name}: {kit.total_packages} packages")
# Search for something specific
results = await mk.search_packages(
"http://seller.example.com:8001",
query="premium video sports",
)
for pkg in results:
print(f" {pkg.name} — {pkg.price_range}")
# Note the product_id for step 3
Look for two things on each package:
negotiation_enabled--- IfTrue, you can negotiate the price down (Step 4).product_idon the placements --- You'll need this to request a quote.
See Media Kit for the full API.
Step 3: Request a Quote¶
A quote is a non-binding price from the seller. It locks in pricing for a limited time.
from ad_buyer.models.deals import QuoteRequest, BuyerIdentityPayload
quote = await client.request_quote(QuoteRequest(
product_id="prod-premium-video",
deal_type="PD", # PD = Preferred Deal
impressions=500_000,
flight_start="2026-07-01",
flight_end="2026-09-30",
target_cpm=25.0, # Your ideal price (optional hint)
buyer_identity=BuyerIdentityPayload(
agency_id="omnicom-456", # Reveal identity for better pricing
advertiser_id="coca-cola",
),
))
print(f"Quote ID: {quote.quote_id}")
print(f"Base CPM: ${quote.pricing.base_cpm}")
print(f"Your price: ${quote.pricing.final_cpm} (tier: {quote.buyer_tier})")
print(f"Expires: {quote.expires_at}")
Deal types:
| Type | Code | Description |
|---|---|---|
| Preferred Deal | PD |
First-look access at a fixed CPM, non-guaranteed |
| Programmatic Guaranteed | PG |
Guaranteed volume at a fixed CPM |
| Private Auction | PA |
Invitation-only auction with a floor price |
Quotes expire
The seller sets an expiry on each quote. Book the deal before it expires, or you'll need to request a new quote.
Step 4: Negotiate (Optional)¶
If the seller supports negotiation for your tier and you want a better price, negotiate before booking.
from ad_buyer.negotiation.client import NegotiationClient
from ad_buyer.negotiation.strategies.simple_threshold import SimpleThresholdStrategy
neg_client = NegotiationClient(api_key="your-api-key")
strategy = SimpleThresholdStrategy(
target_cpm=20.0, # Our opening offer
max_cpm=30.0, # Walk away above this
concession_step=2.0, # Give $2 per round
max_rounds=5,
)
result = await neg_client.auto_negotiate(
seller_url="http://seller.example.com:8001",
proposal_id=quote.quote_id, # Negotiate on the quote
strategy=strategy,
)
if result.outcome.value == "accepted":
print(f"Negotiated to ${result.final_price} CPM in {result.rounds_count} rounds")
else:
print(f"Walked away — seller wouldn't budge below ${result.rounds[-1].seller_price}")
Who can negotiate?
Only Agency and Advertiser tier buyers. PUBLIC and SEAT tier buyers skip this step. Check with buyer_context.can_negotiate().
For full negotiation details, see Negotiation.
Step 5: Book the Deal¶
Convert your quote into a confirmed deal:
from ad_buyer.models.deals import DealBookingRequest
deal = await client.book_deal(DealBookingRequest(
quote_id=quote.quote_id,
buyer_identity=BuyerIdentityPayload(
agency_id="omnicom-456",
advertiser_id="coca-cola",
),
notes="Q3 awareness campaign — premium video",
))
print(f"Deal ID: {deal.deal_id}")
print(f"Status: {deal.status}")
print(f"Final CPM: ${deal.pricing.final_cpm}")
print(f"Impressions: {deal.terms.impressions:,}")
# OpenRTB parameters for DSP activation
if deal.openrtb_params:
print(f"OpenRTB Deal ID: {deal.openrtb_params.id}")
print(f"Bid floor: ${deal.openrtb_params.bidfloor}")
The seller returns a DealResponse with:
deal_id--- The seller's unique deal identifier.openrtb_params--- Plug these into your DSP's deal targeting. Theidfield is what goes into OpenRTB bid requests.activation_instructions--- Any seller-specific setup steps (e.g., creative specs, trafficking notes).
Step 6: Track Your Deal¶
Use get_deal to check the current status of a deal:
deal = await client.get_deal("deal-abc123")
print(f"Status: {deal.status}") # proposed, active, rejected, expired, completed
For persistent tracking across sessions, attach a DealStore:
from ad_buyer.storage.deal_store import DealStore
store = DealStore("sqlite:///./deals.db")
store.connect()
# Attach to the client — quotes and deals auto-persist
client = DealsClient(
seller_url="http://seller.example.com:8001",
api_key="your-api-key",
deal_store=store,
)
# ... request quotes and book deals as normal ...
# Later, query your local store
all_deals = store.list_deals(status="booked")
for d in all_deals:
print(f" {d['product_name']} — ${d['price']} CPM — {d['status']}")
# Get negotiation history
rounds = store.get_negotiation_history(d["id"])
# Get status transition audit log
history = store.get_status_history("deal", d["id"])
The DealStore automatically records:
- Quotes as deals with status
quoted - Booked deals with status
booked - Status transitions with timestamps and trigger info
- Negotiation rounds (when recorded separately)
- Booking records for line items
Using DealBookingFlow (Campaign-Level)¶
DealBookingFlow is the campaign-level orchestrator. Give it a campaign brief and it handles everything: budget allocation across channels, parallel inventory research, human approval, and booking execution.
Use this when you have a campaign budget and want the system to figure out what to buy.
from ad_buyer.flows.deal_booking_flow import DealBookingFlow
from ad_buyer.clients.opendirect_client import OpenDirectClient
from ad_buyer.storage.deal_store import DealStore
# Setup
client = OpenDirectClient(base_url="http://seller.example.com:8001")
store = DealStore("sqlite:///./deals.db")
store.connect()
flow = DealBookingFlow(client=client, store=store)
# Define your campaign
flow.state.campaign_brief = {
"name": "Q3 Awareness Push",
"objectives": ["brand_awareness"],
"budget": 25000,
"start_date": "2026-07-01",
"end_date": "2026-09-30",
"target_audience": {
"demographics": {"age": "18-34"},
"interests": ["gaming", "technology"],
},
"kpis": {"target_cpm": 10},
}
# Run — this kicks off the full pipeline
result = flow.kickoff()
What happens inside:
- Brief validation --- Checks required fields (objectives, budget, dates, audience).
- Audience planning --- Analyzes targeting requirements, estimates coverage per channel.
- Budget allocation --- Portfolio manager splits budget across channels (branding, CTV, mobile, performance).
- Parallel research --- Channel specialists research inventory simultaneously.
- Consolidation --- All recommendations are gathered for review.
- Approval checkpoint --- Waits for human approval (or auto-approves).
- Booking execution --- Books approved recommendations.
Approving recommendations:
After the flow reaches awaiting_approval status:
# Check what's pending
status = flow.get_status()
print(f"Status: {status['execution_status']}")
print(f"Pending: {status['pending_approvals']} recommendations")
# Approve specific products
result = flow.approve_recommendations(["prod_001", "prod_003"])
# Or approve everything
result = flow.approve_all()
print(f"Booked {result['booked']} lines, total cost: ${result['total_cost']}")
Via the REST API:
# Start a booking
curl -X POST http://localhost:8001/bookings \
-H "Content-Type: application/json" \
-d '{
"brief": {
"name": "Q3 Awareness Push",
"objectives": ["brand_awareness"],
"budget": 25000,
"start_date": "2026-07-01",
"end_date": "2026-09-30",
"target_audience": {"demographics": {"age": "18-34"}}
}
}'
# Poll for status
curl http://localhost:8001/bookings/{job_id}
# Approve all recommendations
curl -X POST http://localhost:8001/bookings/{job_id}/approve-all
Using DSPDealFlow (Single-Deal, Direct Mode)¶
DSPDealFlow is for when you know roughly what you want and just need a Deal ID. It discovers inventory, picks the best match, and requests a deal --- all in one shot.
Use this for single-deal, targeted acquisition rather than full-campaign planning.
from ad_buyer.flows.dsp_deal_flow import run_dsp_deal_flow
from ad_buyer.models.buyer_identity import BuyerIdentity, DealType
from ad_buyer.storage.deal_store import DealStore
store = DealStore("sqlite:///./deals.db")
store.connect()
result = await run_dsp_deal_flow(
request="Premium sports video inventory for Q3 awareness campaign",
buyer_identity=BuyerIdentity(
seat_id="ttd-seat-123",
agency_id="omnicom-456",
),
deal_type=DealType.PREFERRED_DEAL,
impressions=500_000,
max_cpm=30.0,
flight_start="2026-07-01",
flight_end="2026-09-30",
store=store,
)
print(f"Status: {result['status']['status']}")
print(f"Deal: {result['status']['deal_response']}")
Flow steps:
- Receive request --- Validates the natural-language request and buyer context.
- Discover inventory --- Searches the seller's catalog for matches.
- Evaluate and select --- Uses a DSP agent (CrewAI) to pick the best product.
- Request Deal ID --- Calls the seller's deal endpoint for the selected product.
Key difference from DealBookingFlow:
| DealBookingFlow | DSPDealFlow | |
|---|---|---|
| Scope | Full campaign, multiple channels | Single deal |
| Input | Campaign brief with budget | Natural language request |
| Selection | Multi-channel specialists | Single DSP agent |
| Approval | Human checkpoint | Automatic |
| Output | Multiple booked lines | One Deal ID |
Handling Errors and Retries¶
DealsClient Errors¶
The DealsClient raises DealsClientError for all API and transport failures:
from ad_buyer.clients.deals_client import DealsClient, DealsClientError
try:
quote = await client.request_quote(quote_request)
except DealsClientError as e:
print(f"Error: {e}")
print(f"HTTP status: {e.status_code}") # 0 for transport errors
print(f"Error code: {e.error_code}") # Machine-readable (e.g. "timeout")
print(f"Detail: {e.detail}") # Human-readable message
Built-in retry logic:
The client automatically retries on transient server errors (502, 503, 504) and timeouts, up to 3 attempts by default:
# Customize retry and timeout behavior
client = DealsClient(
seller_url="http://seller.example.com:8001",
api_key="your-api-key",
timeout=60.0, # 60 seconds (default: 30)
max_retries=5, # 5 attempts (default: 3)
)
What gets retried vs. what doesn't:
| Error | Retried? |
|---|---|
| 502 Bad Gateway | Yes |
| 503 Service Unavailable | Yes |
| 504 Gateway Timeout | Yes |
| Request timeout | Yes |
| 400 Bad Request | No |
| 401 Unauthorized | No |
| 404 Not Found | No |
| 409 Conflict | No |
| Connection refused | No |
Expired Quotes¶
Quotes have a limited lifetime. If you try to book an expired quote, you'll get a 409 or 400 error. The fix is simple --- request a new quote:
try:
deal = await client.book_deal(DealBookingRequest(quote_id=old_quote_id))
except DealsClientError as e:
if e.error_code == "quote_expired":
# Re-quote and try again
new_quote = await client.request_quote(original_request)
deal = await client.book_deal(DealBookingRequest(quote_id=new_quote.quote_id))
DealStore Persistence Errors¶
The DealsClient and flow classes treat persistence as best-effort. If the DealStore fails (disk full, locked, etc.), the API call still succeeds --- you just lose the local record. Errors are logged but never re-raised.
Tips and Best Practices¶
Start with the media kit. Browse before you quote. The media kit shows you what's available, at what price range, and whether negotiation is enabled. Going straight to request_quote with a random product ID will likely fail.
Reveal your identity for better pricing. Pass buyer_identity on both the quote request and the booking request. The seller uses this to calculate tier-based discounts. Going from PUBLIC to ADVERTISER tier can save 15% on CPM.
Use the DealStore. Attach a DealStore to your DealsClient for automatic persistence. It costs nothing if the DB is healthy, and gives you a complete audit trail of every quote, deal, and status change.
Check negotiation_enabled before negotiating. Not all packages support negotiation, and not all tiers can negotiate. Attempting to negotiate when it's not available wastes time and may return errors.
Book promptly. Quotes expire. If you're running a multi-step workflow (browse, quote, negotiate, book), don't let the quote sit too long. The seller may have reserved inventory capacity that times out.
Use DealBookingFlow for campaigns, DSPDealFlow for spot buys. If you have a campaign brief with a budget to allocate across channels, use DealBookingFlow. If you just need a single Deal ID for a specific product, use DSPDealFlow or the DealsClient directly.
Always use async context managers. The DealsClient holds an HTTP connection pool. Use async with to ensure clean shutdown:
async with DealsClient(seller_url=url, api_key=key) as client:
# ... do your work ...
# Connection pool is closed automatically
Related¶
- Negotiation --- Multi-turn price negotiation strategies
- Media Kit --- Browsing seller inventory
- Bookings API --- REST API for the booking workflow
- Authentication --- API key setup and access tiers
- Seller Agent Integration --- Connecting to a seller