ISF 10+2 Data Assembly

ISF 10+2 data assembly collects the ten importer-supplied and two carrier-supplied data elements required under 19 CFR 149 and files them with CBP at least 24 hours before ocean cargo is laden aboard a vessel at the foreign port. Part of the Compliance Reporting & ACE Transmission area, ISF assembly is where parsed trade documents stop being an internal record and become a legally-timed transmission to a customs authority. The Importer Security Filing — universally called “10+2” for its split of ten importer elements and two carrier elements — is the earliest structured obligation in the ocean import lifecycle: it must be complete and accurate before the goods even leave the origin port, well ahead of the entry summary that follows arrival. For the trade-compliance officer, the filing is a bonded declaration whose lateness or inaccuracy carries liquidated damages of up to USD 5,000 per violation. For the Python ETL team that builds the assembler, that same filing is a deterministic function of a bill of lading, a purchase order, and the commercial documents already parsed upstream — no manual re-keying, no guessing at a missing party, no clock that starts late.

Problem Framing: The Filing Is Due Before You Have Everything

The structural difficulty of ISF is that its deadline is anchored to an event — vessel lading at the foreign port — that happens before the importer typically holds a finalized entry. You are assembling a legal declaration from documents that arrive at different times, in different formats, from different parties, and you must close the filing before the vessel is loaded. Three failure modes recur in hand-built ISF workflows.

First, the missing party. The ten importer elements name six distinct parties — manufacturer, seller, buyer, ship-to, container stuffing location, and consolidator — and a booking confirmation frequently omits one or conflates two (a seller who is also the manufacturer, a ship-to that is blank because the goods are sold in transit). An assembler that silently substitutes a default party files a false declaration.

Second, the late clock. The 24-hour rule is measured against the vessel’s lading time abroad, not against your booking or your data-readiness. Teams that trigger assembly off document arrival rather than off the sailing schedule discover the deadline has already passed when the last element lands. The companion guide on late filings and liquidated damages covers the recovery path, but the assembler’s job is to make lateness detectable early rather than discovered late.

Third, the unvalidated element. An importer of record number in the wrong format, a country of origin that disagrees with the HTSUS chapter, a consignee number that is not on file — each passes a naive “field is present” check and is rejected downstream by ACE, consuming a resubmission cycle against a deadline that does not pause. The assembler below closes these gaps by treating every element as a typed, validated, sourced value, and by binding the whole filing to the sailing clock from the first pass.

ISF 10+2 assembly flow from source documents to timed CBP transmission A top-down pipeline diagram. Four source documents feed the assembler along the top: the bill of lading, the purchase order, the commercial invoice, and the packing list. They flow into a single assembly and validation bar that normalizes, cross-references, and validates the data. The assembler produces two grouped outputs. On the left, a teal panel lists the ten importer security-filing elements required by 19 CFR 149.3(a): manufacturer or supplier, seller, buyer, ship-to party, container stuffing location, consolidator or stuffer, importer of record number, consignee number, country of origin, and HTSUS number. On the right, a gold panel lists the two carrier elements under 19 CFR 149.4: the vessel stow plan and the container status messages. Both panels flow down into a gold timing gate that requires the filing at least twenty-four hours before vessel lading at the foreign port, and only then into transmission to CBP through ACE and ABI. The importer elements are shaded teal, the carrier elements gold, and the timing gate gold to mark it as the hard deadline. Bill of lading Purchase order Commercial invoice Packing list ISF 10+2 assembly · normalize · cross-reference · validate 10 importer elements 19 CFR 149.3(a) — importer of record responsibility 1 · Manufacturer / supplier 2 · Seller 3 · Buyer 4 · Ship-to party 5 · Container stuffing loc. 6 · Consolidator (stuffer) 7 · Importer of record no. 8 · Consignee number(s) 9 · Country of origin 10 · HTSUS number 2 carrier elements 19 CFR 149.4 — vessel carrier 11 · Vessel stow plan carrier files ≤ 48 h after lading 12 · Container status msgs CSM events, as generated Timing gate file ≥ 24 h before vessel lading abroad Transmit to CBP · ACE / ABI
The twelve ISF elements are assembled from four source documents, gated on the 24-hour-before-lading deadline, then transmitted to CBP through ACE. Ten elements are the importer's responsibility; two are the carrier's.

Schema / Data Contract

The ISF data contract has to do two things at once: enforce the CBP field-level rules for each of the twelve elements, and record where each value came from so a filing can be defended and a discrepancy traced back to its source document. Every element is therefore a typed value carrying provenance — the source document, the field it was read from, and a confidence flag — rather than a bare string. The Pydantic model below is the input contract for one filing; a booking that cannot satisfy it is rejected at the boundary rather than transmitted and bounced.

from __future__ import annotations
import logging
from datetime import datetime, timezone
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field, field_validator

logger = logging.getLogger(__name__)


class SourceDoc(str, Enum):
    BILL_OF_LADING = "BILL_OF_LADING"
    PURCHASE_ORDER = "PURCHASE_ORDER"
    COMMERCIAL_INVOICE = "COMMERCIAL_INVOICE"
    PACKING_LIST = "PACKING_LIST"


class SourcedValue(BaseModel):
    """A single ISF field value with the provenance needed to defend it."""
    value: str
    source: SourceDoc
    source_field: str
    verified: bool = False


class Party(BaseModel):
    name: SourcedValue
    address: SourcedValue
    country: SourcedValue  # ISO 3166-1 alpha-2


class ImporterSecurityFiling(BaseModel):
    # --- identity + timing ---
    isf_control_number: str = Field(..., pattern=r"^[A-Z0-9]{10,15}$")
    bill_of_lading_number: str = Field(..., min_length=6)
    estimated_lading_utc: datetime  # vessel lading at foreign port
    # --- 10 importer elements (19 CFR 149.3(a)) ---
    manufacturer: Party
    seller: Party
    buyer: Party
    ship_to: Party
    stuffing_location: SourcedValue
    consolidator: Party
    importer_of_record_no: SourcedValue     # EIN / CBP-assigned / SSN
    consignee_numbers: list[SourcedValue] = Field(..., min_length=1)
    country_of_origin: SourcedValue         # ISO 3166-1 alpha-2
    htsus_number: SourcedValue              # 6 to 10 digits
    # --- 2 carrier elements (19 CFR 149.4) ---
    vessel_stow_plan_ref: Optional[str] = None
    container_status_ref: Optional[str] = None

    @field_validator("htsus_number")
    @classmethod
    def _hts_digits(cls, v: SourcedValue) -> SourcedValue:
        digits = v.value.replace(".", "")
        if not (digits.isdigit() and 6 <= len(digits) <= 10):
            raise ValueError(f"HTSUS number must be 6-10 digits, got {v.value!r}")
        return v

    @field_validator("country_of_origin")
    @classmethod
    def _iso_country(cls, v: SourcedValue) -> SourcedValue:
        if len(v.value) != 2 or not v.value.isalpha():
            raise ValueError(f"country_of_origin must be ISO alpha-2, got {v.value!r}")
        return v.model_copy(update={"value": v.value.upper()})

The two carrier elements — the vessel stow plan and the container status messages — are stored as references rather than embedded values because the ocean carrier, not the importer, files them directly with CBP under 19 CFR 149.4. The importer’s assembler holds the linkage so the filing can be reconciled, but it never fabricates the carrier’s data. That split is the whole meaning of “10+2”: ten elements the importer owns, two the carrier owns.

Persisted, the twelve elements live in a normalized table keyed on the ISF control number. The DDL below targets PostgreSQL and stores each element alongside its source document so provenance survives to the audit record.

CREATE TABLE isf_filing (
    isf_control_number   TEXT PRIMARY KEY,
    bill_of_lading_no    TEXT NOT NULL,
    estimated_lading_utc TIMESTAMPTZ NOT NULL,
    status               TEXT NOT NULL DEFAULT 'DRAFT',   -- DRAFT|VALIDATED|FILED|ACCEPTED|REJECTED
    filed_at_utc         TIMESTAMPTZ,
    created_at_utc       TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT status_chk CHECK (status IN
        ('DRAFT','VALIDATED','FILED','ACCEPTED','REJECTED'))
);

CREATE TABLE isf_element (
    isf_control_number   TEXT NOT NULL REFERENCES isf_filing(isf_control_number),
    element_no           SMALLINT NOT NULL CHECK (element_no BETWEEN 1 AND 12),
    element_name         TEXT NOT NULL,        -- 'manufacturer','htsus_number',...
    element_value        TEXT NOT NULL,
    source_doc           TEXT NOT NULL,        -- BILL_OF_LADING|PURCHASE_ORDER|...
    source_field         TEXT NOT NULL,
    verified             BOOLEAN NOT NULL DEFAULT FALSE,
    PRIMARY KEY (isf_control_number, element_no)
);

-- one partial index to find filings whose deadline is approaching
CREATE INDEX isf_due_soon
    ON isf_filing (estimated_lading_utc)
    WHERE status IN ('DRAFT','VALIDATED');

The element_no check constraint pins the table to exactly the twelve-element universe; a row numbered outside 1–12 is a coding error, not data. The partial index on estimated_lading_utc is what a scheduler queries to find filings whose 24-hour window is closing, so the deadline is enforced by the database rather than by hope.

Step-by-Step Implementation

Assembly runs in ordered stages. Each stage reads from already-parsed upstream documents, populates a slice of the twelve elements, and records provenance. The order matters: parties first (they anchor the filing), then the classification and origin elements that depend on the commercial invoice, then the timing computation, and validation last.

Stage 1 — Resolve the six parties from the bill of lading and purchase order. The bill of lading carries the shipper, consignee, and notify party; the purchase order carries buyer and seller; the manufacturer is frequently on neither and must be pulled from the commercial invoice. Because the same party can appear in more than one document, the resolver prefers the most authoritative source per element and records which one it used. The detailed field-by-field crosswalk lives in Mapping bill of lading data to ISF 10+2 fields.

def resolve_party(name: str, bol: dict, po: dict, invoice: dict,
                  preference: list[SourceDoc]) -> Party:
    """Pull one party from the first source that carries it, in preference order."""
    lookup = {
        SourceDoc.BILL_OF_LADING: bol,
        SourceDoc.PURCHASE_ORDER: po,
        SourceDoc.COMMERCIAL_INVOICE: invoice,
    }
    for src in preference:
        doc = lookup[src]
        block = doc.get(name)
        if block and block.get("name") and block.get("address"):
            return Party(
                name=SourcedValue(value=block["name"], source=src, source_field=name),
                address=SourcedValue(value=block["address"], source=src, source_field=name),
                country=SourcedValue(value=block["country"], source=src, source_field=name),
            )
    raise ValueError(f"ISF party '{name}' not found in any source document")

Stage 2 — Carry the classification and origin elements from the commercial documents. The HTSUS number and the country of origin are not on the bill of lading; they come from the classified commercial invoice, which is produced upstream by Commercial Invoice PDF Extraction and cross-checked against normalized quantities from Packing List Data Normalization. Reusing those parsed values — rather than re-reading the PDF here — is what keeps assembly deterministic.

def carry_classification(invoice: dict) -> tuple[SourcedValue, SourcedValue]:
    hts = SourcedValue(
        value=invoice["hts_code"],
        source=SourceDoc.COMMERCIAL_INVOICE,
        source_field="hts_code",
    )
    origin = SourcedValue(
        value=invoice["country_of_origin"],
        source=SourceDoc.COMMERCIAL_INVOICE,
        source_field="country_of_origin",
    )
    return hts, origin

Stage 3 — Compute the filing deadline from the sailing schedule. The deadline is 24 hours before the vessel is laden at the foreign port, so the assembler subtracts the window from the estimated lading time and stores both. Anchoring to lading — not to booking or data-readiness — is the single most important line in the whole pipeline.

from datetime import timedelta

ISF_LEAD = timedelta(hours=24)

def compute_deadline(estimated_lading_utc: datetime) -> datetime:
    """The ISF must be on file no later than 24 h before lading abroad."""
    if estimated_lading_utc.tzinfo is None:
        raise ValueError("estimated_lading_utc must be timezone-aware (UTC)")
    return estimated_lading_utc - ISF_LEAD

Stage 4 — Assemble and validate the full filing. With parties, classification, origin, and timing in hand, the assembler constructs the ImporterSecurityFiling model. Construction is validation: Pydantic runs every field validator, so a malformed HTSUS number or a non-ISO country raises before the record can be marked ready to file.

def assemble_isf(control_no: str, bol: dict, po: dict, invoice: dict) -> ImporterSecurityFiling:
    party_pref = [SourceDoc.BILL_OF_LADING, SourceDoc.PURCHASE_ORDER,
                  SourceDoc.COMMERCIAL_INVOICE]
    hts, origin = carry_classification(invoice)
    filing = ImporterSecurityFiling(
        isf_control_number=control_no,
        bill_of_lading_number=bol["bol_number"],
        estimated_lading_utc=bol["estimated_lading_utc"],
        manufacturer=resolve_party("manufacturer", bol, po, invoice,
                                    [SourceDoc.COMMERCIAL_INVOICE, SourceDoc.PURCHASE_ORDER]),
        seller=resolve_party("seller", bol, po, invoice, party_pref),
        buyer=resolve_party("buyer", bol, po, invoice, party_pref),
        ship_to=resolve_party("ship_to", bol, po, invoice, party_pref),
        stuffing_location=SourcedValue(value=bol["stuffing_location"],
                                       source=SourceDoc.BILL_OF_LADING,
                                       source_field="stuffing_location"),
        consolidator=resolve_party("consolidator", bol, po, invoice, party_pref),
        importer_of_record_no=SourcedValue(value=po["ior_number"],
                                           source=SourceDoc.PURCHASE_ORDER,
                                           source_field="ior_number"),
        consignee_numbers=[SourcedValue(value=po["consignee_no"],
                                        source=SourceDoc.PURCHASE_ORDER,
                                        source_field="consignee_no")],
        country_of_origin=origin,
        htsus_number=hts,
    )
    logger.info("ISF assembled control=%s deadline=%s",
                control_no, compute_deadline(filing.estimated_lading_utc).isoformat())
    return filing

The assembler never emits a partial filing. If any of the twelve elements cannot be resolved, resolve_party raises, the model does not construct, and the record stays in DRAFT — visible on the due-soon index but explicitly not ready to transmit. That is the correct behavior: a missing element that surfaces now, days before lading, is recoverable; the same gap discovered after the deadline is a liquidated-damages claim.

Validation & Determinism

Validation happens in two layers, and both must pass before a filing leaves VALIDATED. The first layer is structural, enforced by the Pydantic model: the HTSUS number is six to ten digits, the country of origin is an ISO alpha-2 code, the importer of record number matches an accepted format, and at least one consignee number is present. The second layer is cross-element consistency, which no single field validator can catch.

def cross_validate(filing: ImporterSecurityFiling) -> list[str]:
    """Return a list of consistency errors; empty means the filing is coherent."""
    errors: list[str] = []
    # Country of origin should be plausible against the HTSUS chapter's scope.
    if filing.country_of_origin.value == filing.ship_to.country.value:
        logger.info("origin equals ship-to country for %s", filing.isf_control_number)
    # Manufacturer country and country of origin must not silently disagree.
    if filing.manufacturer.country.value != filing.country_of_origin.value:
        errors.append(
            f"manufacturer country {filing.manufacturer.country.value} "
            f"!= country_of_origin {filing.country_of_origin.value}")
    # Deadline must still be in the future at validation time.
    deadline = compute_deadline(filing.estimated_lading_utc)
    if deadline <= datetime.now(timezone.utc):
        errors.append(f"ISF deadline {deadline.isoformat()} already passed")
    return errors

Determinism is the property an auditor tests: the same three source documents must assemble to the same twelve elements on every replay, with the same provenance recorded. The assembler guarantees this by never introducing time-varying or heuristic behavior into the value derivation — the only clock it reads is the stored estimated lading time, not the wall clock, so re-running assembly a week later reproduces the identical filing. The one intentionally time-sensitive check, the passed-deadline test, lives in cross_validate and is labeled as such, keeping the pure assembly path replayable. Every element carries its source and source_field, so a discrepancy is always traceable to a specific line of a specific document rather than to the assembler’s judgment.

Downstream Integration

A validated ISF is not the end of the record’s life — it is the opening move of a shipment’s compliance history, and several downstream systems read it. The assembled filing transmits through the same ACE/ABI channel that carries the entry, so a rejection returns as an X12 824 application advice; decoding and recovering from those rejections is handled by ACE and ABI Rejection Handling, and an ISF rejected for a bad element re-enters this assembler at the failing stage rather than being re-keyed by hand.

The twelve elements also seed the entry that follows arrival. The importer of record number, consignee number, country of origin, and HTSUS number captured here flow forward into Entry Summary Filing Schemas, where the same classification is reconciled against the CBP 7501 line items. Because the ISF was assembled from the same parsed commercial documents the entry will use, a divergence between the ISF HTSUS number and the entered classification is a signal worth flagging — it usually means a reclassification happened between filing and entry, which is exactly the kind of change the post-entry workflows exist to reconcile. When lateness cannot be avoided — a vessel sailing schedule that compresses faster than the data arrives — the record is routed to Handling late ISF filings and liquidated damages for mitigation rather than dropped.

Scaling & Resilience

A single broker files ISFs for thousands of ocean bookings a week, each on its own sailing clock, and the scaling problem is not throughput so much as timeliness under fan-out: every filing has an independent deadline, and none of them wait. The controls that keep the assembler inside its window:

  • Deadline-driven scheduling rather than arrival-driven. A worker polls the isf_due_soon partial index on a short interval, pulling filings ordered by estimated_lading_utc so the most urgent assemble first. The clock, not the inbox, sets priority.
  • Idempotent assembly keyed on the ISF control number. Re-running assembly for a control number that is already FILED is a no-op; a booking whose documents were revised produces an amendment against the same control number rather than a duplicate transmission.
  • Bounded concurrency on the ACE channel. A semaphore caps simultaneous transmissions so a surge of due filings cannot overwhelm the ABI connection, while the deadline ordering guarantees the most urgent go first when the channel is saturated.
  • Circuit breaking on upstream document feeds. If commercial-invoice parsing stalls, the assembler holds affected filings in DRAFT and alerts on those whose deadline is approaching, rather than transmitting incomplete records against the clock.

Transient failures — a momentary ACE outage, a database timeout — are retried with backoff, but a filing is never transmitted from a partially assembled state. The database status column is the single source of truth for where each filing sits, so a worker that crashes mid-transmission resumes from VALIDATED rather than re-deriving values or double-filing.

Compliance Obligations

The ISF is a bonded declaration, and every filing becomes part of the importer’s compliance record for the CBP five-year recordkeeping horizon. The assembler treats the stored filing as evidence: each of the twelve elements is retained with its source document, source field, the assembly timestamp, and the computed deadline, so a Focused Assessment or an ISF liquidated-damages inquiry can be answered from primary records rather than reconstructed. The isf_element rows are written append-style — an amendment supersedes rather than overwrites, preserving the exact values that were on file at each point in time.

The core obligation the assembler exists to protect is timeliness. Under 19 CFR 149, the ten importer elements must be transmitted no later than 24 hours before the cargo is laden aboard the vessel at the foreign port; the two carrier elements — the vessel stow plan and the container status messages — are the carrier’s responsibility on their own schedules. A late or inaccurate ISF exposes the importer to liquidated damages of up to USD 5,000 per violation and, on repeated findings, to increased cargo examinations and loss of trusted-trader standing. By anchoring assembly to the sailing clock, validating every element structurally and cross-sectionally before transmission, and recording provenance for each of the twelve, the assembler turns a filing that is due before the data is complete into infrastructure that files on time, files correctly, and survives audit rather than merely clearing the vessel.

Authoritative references: 19 CFR Part 149 (Importer Security Filing); CBP Importer Security Filing “10+2” program; SAFE Port Act of 2006; HTSUS (USITC); WCO Harmonized System 2022.

Up: Compliance Reporting & ACE Transmission