Syncing promo codes across OTA channels
A promotion that goes live on the direct site at 09:00 but does not reach an OTA until 09:40 is a forty-minute parity violation with your name on it. Syncing promo codes across channels is the problem of activating and deactivating a campaign everywhere it is in scope, atomically enough that no channel ever advertises a discount another channel is missing. This how-to sits under Promotional Discount Orchestration in the Dynamic Pricing Rule Engines & Optimization pillar, and it leans on the delivery mechanics established in the ingestion pillar.
Prerequisites
- Python 3.11+ and an async runtime (
asyncio) httpxfor async channel calls (already a dependency of the ingestion stack)- Channel adapters exposing activate/deactivate for a campaign, per the Channel Manager Integration Patterns contract
- A campaign store with scope, discount, and validity window fields
- Idempotency keys so a retried activation never double-applies
Step 1 — Model the campaign and its channel scope
A campaign carries the channels it targets and a single idempotency key reused across retries. The scope must never include a channel that would break the parity obligations governed by Rate Parity Compliance Across Booking Channels.
from __future__ import annotations
import asyncio
import logging
from dataclasses import dataclass
import httpx
logger = logging.getLogger("pricing.promo_sync")
@dataclass(frozen=True)
class Campaign:
campaign_id: str
percent_off: float
channels: tuple[str, ...]
idempotency_key: str
Step 2 — Activate on all channels concurrently, then verify
Fan the activation out concurrently for speed, but treat the campaign as live only once every channel acknowledges. Gather results and detect partial success explicitly rather than assuming the whole set succeeded.
async def activate_on_channel(client: httpx.AsyncClient, base_url: str,
campaign: Campaign) -> bool:
try:
resp = await client.post(
f"{base_url}/promotions",
json={"campaign_id": campaign.campaign_id, "percent_off": campaign.percent_off},
headers={"Idempotency-Key": campaign.idempotency_key},
timeout=10.0,
)
resp.raise_for_status()
return True
except httpx.HTTPError as exc:
logger.error("activation failed on %s: %s", base_url, exc)
return False
async def activate_campaign(campaign: Campaign,
endpoints: dict[str, str]) -> dict[str, bool]:
async with httpx.AsyncClient() as client:
results = await asyncio.gather(*(
activate_on_channel(client, endpoints[ch], campaign)
for ch in campaign.channels
))
status = dict(zip(campaign.channels, results))
if not all(status.values()):
failed = [ch for ch, ok in status.items() if not ok]
logger.warning("partial activation for %s; failed: %s",
campaign.campaign_id, failed)
return status
Step 3 — Reconcile partial failures before declaring the campaign live
Do not flip the campaign to “active” in your own store until every channel confirms. On partial failure, retry the missing channels with the same idempotency key — safe precisely because the key makes a repeat activation a no-op on channels that already succeeded.
async def sync_until_consistent(campaign: Campaign, endpoints: dict[str, str],
max_rounds: int = 3) -> bool:
pending = set(campaign.channels)
for round_no in range(1, max_rounds + 1):
subset = Campaign(campaign.campaign_id, campaign.percent_off,
tuple(pending), campaign.idempotency_key)
status = await activate_campaign(subset, endpoints)
pending = {ch for ch, ok in status.items() if not ok}
if not pending:
logger.info("campaign %s consistent after round %d",
campaign.campaign_id, round_no)
return True
await asyncio.sleep(2 ** round_no) # backoff between rounds
logger.error("campaign %s still inconsistent: %s", campaign.campaign_id, pending)
return False
Gating the “active” flag on full consistency is the whole trick: a campaign that is live in your database but missing on one OTA is exactly the state that produces a parity complaint.
Verification and testing
Assert that a campaign is only marked live when every channel acknowledges, using a stub that fails one channel once.
import asyncio
def test_partial_then_consistent(monkeypatch) -> None:
calls = {"ota_a": 0}
async def fake(campaign, endpoints):
calls["ota_a"] += 1
ok_a = calls["ota_a"] > 1 # ota_a fails on first attempt
return {"direct": True, "ota_a": ok_a}
monkeypatch.setattr("pricing.promo_sync.activate_campaign", fake)
campaign = Campaign("SUMMER25", 0.25, ("direct", "ota_a"), "idem-1")
ok = asyncio.run(sync_until_consistent(campaign, {}))
assert ok is True
assert calls["ota_a"] == 2
Common pitfalls and edge cases
- Marking live on partial success. The most common parity bug; require all-channel acknowledgment first.
- Missing idempotency key. Retries then double-apply the discount on channels that already succeeded.
- Deactivation drift. Expiries can be lost like activations; reconcile active campaigns on a schedule, not only on events.
- Clock skew across channels. Schedule activation by a shared UTC instant, not each channel’s local clock.
- Unbounded retries. Cap the rounds and alert on persistent inconsistency instead of looping forever.
Related
- Promotional Discount Orchestration — the parent cluster where net rates are computed before fan-out.
- Webhook vs REST Sync Patterns — the delivery-mechanism decision behind campaign propagation.
- Deduplicating webhook events with idempotency keys — the same idempotency discipline applied to inbound events.