Standardizing Billing Codes Across Traffic Systems

A billing code is the single identifier that ties a sold placement to the money it owes, and in most broadcast operations that identifier arrives in a different shape from every system that touches it. Agency portals emit one delimiter convention, a legacy traffic system pads segments with fixed-width whitespace, a direct-advertiser API sends lowercase, and the billing or ERP ledger expects an uppercase canonical form that none of them produce. When those variants are not reconciled at the ingestion edge, the same physical order fractures into several apparent codes: inventory is allocated under one, revenue is posted under another, and reconciliation surfaces the gap weeks later as an unbillable spot or a disputed invoice. This guide sits under Broadcast Traffic Architecture & Taxonomy and specifies the deterministic normalization and validation layer that collapses every inbound variant into one canonical code, written for traffic managers who need the operational framing and for Python automation developers who need deployable, production-grade code.

Concept & Data Model

A billing code is not an opaque string; it is a composite key whose segments each carry meaning the scheduler and the ledger both depend on. Standardization means parsing that composite into typed segments, remapping deprecated values against a canonical registry, and re-emitting a single normalized form with explicit lineage back to the raw input. The canonical entity hierarchy runs spot → avail → order → campaign; the billing code is the join key that binds a spot’s placement to the order’s financial obligation, which is why it must be resolved before a record reaches the scheduler that consumes Understanding Broadcast Spot Schemas and Metadata.

A typical billing code decomposes into four ordered segments — for example DDB-04412-PROD07-2026 — each with its own constraint and role. The normalizer parses, validates, and re-serializes these segments; it never treats the concatenated string as the unit of work.

Segment Type Constraint Role in normalization
agency_prefix str (enum) 2–4 A–Z chars; registry-bound Identifies agency of record; deprecated prefixes are remapped, never passed through
advertiser_id str 4–6 digits; FK → advertiser master Anchors revenue attribution to a single advertiser
product_code str alphanumeric, ≤ 8 chars Separates competing products under one advertiser for separation logic
contract_year int 4 digits; within active fiscal window Scopes the code to a rate card and prevents cross-year collisions
revenue_type str (enum) national / local / political / trade Routes the code to the correct ledger and compliance path
canonical_code str uppercase, hyphen-delimited, unique The emitted join key every downstream system agrees on
source_system str (enum) agency / direct-api / legacy Lineage tag; disambiguates cross-system collisions
code_hash str (sha256) content-addressed Idempotency key; deduplicates re-sent orders

Standardization has to be a stateless, idempotent transformation that runs the moment a payload is received — not a step deferred to the scheduling engine. Processing the same raw order twice must yield the same canonical code and the same code_hash, so a re-sent agency file never opens a second financial obligation for one placement. The normalizer parses raw JSON or XML payloads into an isolated staging model, applies deterministic cleansing and registry remapping, validates against the active contract registry, and emits a normalized object with lineage metadata; it never mutates the source payload in place.

Billing-code standardization pipeline: heterogeneous raw codes collapse into one canonical code Three source systems each emit the same physical order in a different shape. An agency portal sends "DDB_04412 / PROD07.2026" with mixed delimiters, a direct advertiser API sends the lowercase "ddb-04412-prod07-2026", and a legacy traffic system sends "DDB 04412 PROD07 2026" padded with fixed-width whitespace. All three feed a stateless, idempotent normalizer that parses segments and collapses delimiters, remaps deprecated values through a version-pinned registry, and validates the advertiser and fiscal window, rejecting failures to a dead-letter queue. The normalizer emits a single canonical billing code, "DDB-04412-PROD07-2026". Contract registry validation then routes that one code to two consumers that join on it: the scheduler as a routing key, and billing and reconciliation as the revenue key. Agency Portal mixed delimiters · deprecated prefix DDB_04412 / PROD07.2026 Direct Advertiser API lowercase canonical drift ddb-04412-prod07-2026 Legacy Traffic System fixed-width whitespace padding DDB  04412  PROD07  2026 Normalizer · idempotent 1 Parse segments collapse delimiters 2 Remap registry deprecated → canonical 3 Validate advertiser · window reject → dead-letter queue ERR_INVALID_SEGMENT_LENGTH Canonical Billing Code DDB-04412-PROD07-2026 Contract Registry Validation one key, two consumers join on it Scheduler routing key → avail Billing & Reconciliation revenue key → ledger

Figure — Billing code standardization: heterogeneous source codes pass through a normalization and remap registry to a single canonical code, which validation then routes to both the scheduler and the billing ledger.

Implementation Approach

Three design decisions dominate a billing-code normalizer: where standardization runs, how deprecated values are resolved, and how failures are surfaced.

Normalize at the ingestion edge, not in the scheduler. The transformation must execute as a stateless layer immediately on payload receipt, before the record enters the reservation queue. Pushing normalization downstream means every consumer re-implements its own cleansing, and the variants diverge; a code standardized once at the boundary is a code every system can join on. This mirrors the boundary discipline used when you map legacy spot IDs to modern schemas — translation is resolved once, at a single controlled point, and cached rather than recomputed inline.

Registry lookup over inline string rewriting. Deprecated agency prefixes, merged advertiser IDs, and renamed product codes cannot be fixed with hard-coded string replacements scattered through the pipeline. They resolve against a version-controlled canonical registry: a read-only mapping from every historical variant to its current canonical segment. Registry lookups run against an isolated, read-only service account rather than the live traffic database, following the same access separation covered in Security Boundaries for Traffic Database Access. Version-controlling the registry keeps silent code drift — a prefix that quietly changes meaning after an agency-of-record switch — out of production, and lets an audit reproduce exactly which mapping was in force on any past date.

Structured rejection over silent drops. When a segment fails validation, the normalizer rejects the payload with a machine-readable error code — ERR_DEPRECATED_PREFIX, ERR_INVALID_SEGMENT_LENGTH, ERR_MISSING_CONTRACT_REF — rather than dropping it or coercing it to a best-guess value. Structured rejection routes the record to a dead-letter queue for automated remediation and leaves a clear audit trail, where a silent coercion would post revenue against the wrong advertiser. The billing code is the primary join key an avails mapper relies on, so a mismatched code misroutes premium inventory — which is why normalization is a precondition for Avails Mapping Strategies for Linear TV, not a downstream cleanup.

Production Python Implementation

The reference module below is a typed, deployable normalizer. Pydantic v2 models enforce the segment contract at the boundary; a version-pinned remap registry resolves deprecated values; every transition emits a structured log line in the traffic-ops format timestamp | level | module | message so billing reconciliation and FCC audit trails can be reconstructed from logs alone. It is deterministic and idempotent on code_hash.

python
from __future__ import annotations

import hashlib
import logging
import re
from datetime import datetime, timezone
from enum import Enum

from pydantic import BaseModel, Field, field_validator

# Traffic-ops structured logging: timestamp | level | module | message
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)
logger = logging.getLogger("billing.normalizer")

# Any run of non-alphanumeric characters is treated as a single delimiter,
# so "DDB_04412 / PROD07.2026" and "DDB-04412-PROD07-2026" parse identically.
_DELIMITER = re.compile(r"[^A-Za-z0-9]+")


class RevenueType(str, Enum):
    NATIONAL = "national"
    LOCAL = "local"
    POLITICAL = "political"   # inherits lowest-unit-charge + public-file duties
    TRADE = "trade"           # barter; zero cash value, must not post as revenue


class NormalizationError(Exception):
    """Carries a machine-readable code so remediation can be automated."""

    def __init__(self, code: str, detail: str) -> None:
        self.code = code
        super().__init__(f"{code}: {detail}")


class RawOrder(BaseModel):
    """An inbound order as received from an external system, before cleansing."""

    raw_billing_code: str = Field(..., min_length=1)
    source_system: str = Field(..., min_length=1)   # agency | direct-api | legacy
    spot_id: str = Field(..., min_length=1)          # correlation id for logging


class CanonicalCode(BaseModel):
    """The single normalized form every downstream system joins on."""

    agency_prefix: str
    advertiser_id: str
    product_code: str
    contract_year: int
    revenue_type: RevenueType
    canonical_code: str
    source_system: str
    code_hash: str

    @field_validator("agency_prefix")
    @classmethod
    def prefix_shape(cls, v: str) -> str:
        if not (2 <= len(v) <= 4 and v.isalpha()):
            raise ValueError("agency_prefix must be 2-4 alphabetic characters")
        return v.upper()


class BillingCodeNormalizer:
    """Collapses heterogeneous inbound billing codes into one canonical code.

    Rules apply in a fixed order so identical inputs always produce an identical
    canonical code and hash; the result is safe to replay during a reconciliation
    audit. Deprecated segments resolve through a version-pinned remap registry."""

    def __init__(
        self,
        prefix_remap: dict[str, str],       # deprecated -> canonical agency prefix
        active_advertisers: frozenset[str],
        active_years: frozenset[int],
        registry_version: str,
    ) -> None:
        self._prefix_remap = prefix_remap
        self._active_advertisers = active_advertisers
        self._active_years = active_years
        self._registry_version = registry_version

    def _parse_segments(self, raw: str) -> list[str]:
        # Strip non-semantic whitespace, collapse any delimiter run, uppercase.
        cleaned = _DELIMITER.sub("-", raw.strip()).upper().strip("-")
        segments = [s for s in cleaned.split("-") if s]
        if len(segments) != 4:
            raise NormalizationError(
                "ERR_INVALID_SEGMENT_LENGTH",
                f"expected 4 segments, got {len(segments)} from {raw!r}",
            )
        return segments

    def _resolve_prefix(self, prefix: str) -> str:
        # A deprecated prefix is remapped to its canonical form, never passed
        # through: an agency-of-record change must not fork one order into two.
        return self._prefix_remap.get(prefix, prefix)

    def _classify_revenue(self, product_code: str) -> RevenueType:
        # Product-code convention flags political and trade inventory, which
        # follow different ledger and compliance paths than commercial sales.
        if product_code.startswith("POL"):
            return RevenueType.POLITICAL
        if product_code.startswith("TRD"):
            return RevenueType.TRADE
        return RevenueType.NATIONAL

    def normalize(self, order: RawOrder) -> CanonicalCode:
        raw_prefix, advertiser_id, product_code, year_str = self._parse_segments(
            order.raw_billing_code
        )

        agency_prefix = self._resolve_prefix(raw_prefix)
        if raw_prefix in self._prefix_remap:
            logger.info(
                "spot_id=%s remapped_prefix %s->%s registry=%s",
                order.spot_id, raw_prefix, agency_prefix, self._registry_version,
            )

        # Validate the advertiser against the master before attributing revenue.
        if advertiser_id not in self._active_advertisers:
            logger.warning(
                "spot_id=%s ERR_MISSING_CONTRACT_REF advertiser_id=%s",
                order.spot_id, advertiser_id,
            )
            raise NormalizationError(
                "ERR_MISSING_CONTRACT_REF",
                f"advertiser {advertiser_id} not in active registry",
            )

        try:
            contract_year = int(year_str)
        except ValueError as exc:
            raise NormalizationError(
                "ERR_INVALID_SEGMENT_LENGTH", f"contract_year not numeric: {year_str!r}"
            ) from exc
        if contract_year not in self._active_years:
            raise NormalizationError(
                "ERR_CONTRACT_YEAR_OUT_OF_WINDOW",
                f"contract_year {contract_year} outside active fiscal window",
            )

        canonical_code = f"{agency_prefix}-{advertiser_id}-{product_code}-{contract_year}"
        # Hash includes source_system so a genuine cross-system collision does not
        # dedupe two distinct obligations onto one hash.
        code_hash = hashlib.sha256(
            f"{canonical_code}|{order.source_system}".encode()
        ).hexdigest()

        result = CanonicalCode(
            agency_prefix=agency_prefix,
            advertiser_id=advertiser_id,
            product_code=product_code,
            contract_year=contract_year,
            revenue_type=self._classify_revenue(product_code),
            canonical_code=canonical_code,
            source_system=order.source_system,
            code_hash=code_hash,
        )
        logger.info(
            "spot_id=%s canonical=%s revenue_type=%s hash=%s accepted=True",
            order.spot_id, canonical_code, result.revenue_type.value, code_hash[:12],
        )
        return result

A representative accepted-path log line reads 2026-07-03T14:22:07+00:00 | INFO | billing.normalizer | spot_id=S-8841 canonical=DDB-04412-PROD07-2026 revenue_type=national hash=9f2c1a4b7e02 accepted=True. Because normalize is pure over its inputs and the pinned registry version, replaying the same order during a reconciliation audit produces an identical canonical code — the property that lets finance prove revenue was attributed by rule rather than manual override.

Validation & Edge Cases

Billing-code standardization generates boundary conditions that a naïve string cleaner mishandles. Each must be an explicit, tested case:

  • Deprecated agency prefix after an agency-of-record change. When an advertiser moves agencies mid-year, historical orders still arrive under the old prefix. The normalizer must remap it to the canonical prefix through the registry, not pass it through — otherwise one advertiser’s spend splits across two apparent agencies and reconciliation cannot net it.
  • Political inventory codes. A code classified political inherits lowest-unit-charge and public-inspection-file obligations the moment it is normalized. It must resolve to a candidate, sponsor, and rate; an unresolved political code is a compliance violation, not a soft warning, and routes to the FCC political file enforcement path rather than the general ledger.
  • Trade and barter codes. A trade code represents zero-cash barter and must never post as cash revenue. Coercing it into the national ledger inflates billed revenue and corrupts reconciliation; the revenue-type classification has to be preserved through every downstream hop.
  • Cross-system code collisions. The same literal string can mean different things in a legacy traffic system and a direct-API feed. The code_hash incorporates source_system so two genuinely distinct obligations are never deduplicated onto one hash, while an honest re-send from the same system still deduplicates correctly.
  • Contract-year rollover at the fiscal boundary. A code emitted on the last day of a fiscal year must bind to the rate card in force at air time, not at ingestion time. Normalizing contract_year against the active fiscal window — rather than the wall clock — prevents a December order from being priced on next year’s card.
  • Creative identifier confused for a billing code. ISCI and Ad-ID creative codes travel alongside billing codes in agency payloads and are frequently swapped by upstream systems. Strict segment-shape validation rejects a creative identifier before it can be attributed as an advertiser, raising ERR_INVALID_SEGMENT_LENGTH rather than silently accepting it.

Integration Points

Billing-code normalization is the ingestion-edge stage: it consumes raw orders and produces one canonical code that both the scheduler and the ledger consume.

Upstream — order ingestion. Raw orders arrive as heterogeneous payloads: EDI from agency portals, JSON from direct-advertiser APIs, and fixed-width or CSV exports from legacy traffic systems. Type coercion and schema enforcement happen before the normalizer’s RawOrder model accepts a record, using the validator pattern documented in Schema Validation with Pydantic for Traffic Data and the format handling in the Avion & Avstar ingestion pipelines. A payload that cannot populate RawOrder is dead-lettered, never guessed.

Downstream — scheduling and billing. The normalizer emits CanonicalCode records over a versioned dispatch contract. The message schema below is what both consumers join on; it carries the source-system lineage and content hash so the scheduler and the ledger can prove they acted on the exact code the audit trail recorded.

json
{
  "canonical_code": "DDB-04412-PROD07-2026",
  "advertiser_id": "04412",
  "revenue_type": "national",
  "source_system": "agency",
  "contract_year": 2026,
  "code_hash": "sha256:9f2c1a4b7e02…",
  "normalized_at": "2026-07-03T14:22:07+00:00",
  "registry_version": "reg-2026.07"
}

The scheduler uses canonical_code as the routing key to bind a spot to its avail; the billing system joins on the same code to attribute revenue. Because both consume one normalized form, a placement and its invoice can never diverge onto two identifiers.

Compliance & Audit Considerations

Billing-code standardization is where several financial and regulatory controls are first enforced, and getting it wrong is expensive.

FCC political-file obligations. A code classified political must resolve to a candidate or issue sponsor, a disclosed rate, and a public-inspection-file entry the moment it is normalized. The normalizer refuses to emit a political code whose sponsor or rate cannot be resolved, because a missing disclosure is a violation rather than a warning. This enforcement point feeds directly into the compliance logic in Spot Scheduling Validation & Rule Engines.

Immutable remap ledger. Every prefix remap, advertiser re-attribution, and structured rejection is appended to a write-once ledger, each entry content-hashed and chained to its predecessor. A correction is a new entry referencing the original; records are never updated in place. This is what makes a revenue-attribution decision defensible under a financial audit, and it is why the normalizer emits a structured log line for every transition.

Reconciliation reproducibility. Because normalization is deterministic and idempotent on code_hash for a pinned registry version, finance can replay any historical order file and obtain a byte-identical canonical code. That reproducibility is the control: it demonstrates that revenue was attributed by rule, and it is what lets a monthly reconciliation net billed revenue against as-run logs without manual matching.

Troubleshooting & Common Errors

When a normalization run misbehaves, these are the named patterns operators hit most often, with root cause and remediation.

Error pattern Diagnostic indicator Root cause Remediation
Deprecated prefix passed through One advertiser’s spend split across two agency prefixes Remap registry stale after an agency-of-record change Publish the new mapping, bump registry_version, re-normalize affected orders
Delimiter drift Same order rejected as ERR_INVALID_SEGMENT_LENGTH from one source only Upstream system changed its delimiter convention Confirm the collapse regex covers the new delimiter; add a fixture and re-run
Revenue misattribution Trade or political spend posted to the national ledger revenue_type dropped between normalizer and billing Restore the full CanonicalCode on the dispatch contract; replay from the ledger checkpoint
Cross-system collision Two distinct orders deduplicated to one obligation code_hash computed without source_system Recompute the hash with lineage included; re-emit both records
Unresolvable contract reference ERR_MISSING_CONTRACT_REF spike after a data load Advertiser master not synced before order file processed Sync the advertiser registry, then reprocess the dead-letter queue

When normalization failures exceed a predefined threshold — a corrupted registry feed or a mass prefix deprecation — a circuit breaker in the automation layer halts ingestion at the pipeline boundary once the error rate is breached, and requires explicit operator acknowledgment before resuming. The breaker state machine is the same one detailed in Spot Scheduling Validation & Rule Engines; wiring the normalizer into it prevents a bad registry from cascading a batch of misattributed codes into billing while engineering isolates the root cause.