Handling late ISF filings and liquidated damages
Every Importer Security Filing carries a hard deadline: the ten importer elements must be on file with CBP no later than 24 hours before the cargo is laden aboard the vessel at the foreign port. Miss it and the filing is not merely late — it exposes the bond to a liquidated-damages claim of up to USD 5,000.00 per violation, and a single container can trigger several. The engineering problem is that “24 hours before lading” is a timezone-aware moment tied to the foreign load port, not to your server clock, and the vessel’s estimated lading time drifts as schedules change. This page builds a monitoring workflow that computes each shipment’s ISF deadline in the correct timezone, classifies every open filing as on-time, at-risk, or breached, emits alerts before the clock runs out, and records the timestamps you will need if a claim is ever mitigated. It leads with runnable code and anchors the timing rule in 19 CFR 149.2.
Prerequisites
The monitor is a pure function of a shipment’s timing facts; confirm each input before you rely on it:
- Python 3.10+ with
zoneinfofrom the standard library for timezone-aware arithmetic, plusdataclass,Optional, andDecimal. Never represent the deadline as a naive datetime — a naive value silently assumes the host timezone and will misclassify any filing near the boundary. - A confirmed estimated time of lading (ETL) per shipment, expressed with its IANA timezone (for example
Asia/Shanghai), sourced from the carrier booking. The load-port local time is what defines the deadline, not the vessel’s departure or the US arrival. - A filing status feed. Each shipment must expose whether its ISF has been accepted, and if so the acceptance timestamp. That status comes from the ISF 10+2 Data Assembly pipeline and its transmission acknowledgements.
- An alert sink (a queue, a pager, or an email relay) already provisioned. This page decides when to alert and at what severity; it does not implement the transport.
- Structured logging configured (stdlib
loggingwith the%(asctime)s | %(levelname)s | %(name)s | %(message)sformat) and a monotonic, auditable clock source so every classification is reproducible after the fact.
Implementation
The monitor models each shipment’s timing facts as a frozen dataclass, computes the deadline as etl - 24h in the load-port timezone, and classifies the filing against the current instant. All arithmetic happens in UTC internally; timezone objects exist only at the edges so comparisons are unambiguous.
import logging
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from enum import Enum
from typing import Optional
from zoneinfo import ZoneInfo
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)
logger = logging.getLogger("isf_deadline_monitor")
ISF_LEAD = timedelta(hours=24) # 19 CFR 149.2: 24h before lading
MAX_LIQUIDATED_DAMAGES = Decimal("5000.00") # USD, per violation ceiling
class FilingState(Enum):
ON_TIME = "ON_TIME" # accepted at or before the deadline
AT_RISK = "AT_RISK" # open, deadline within the warning window
BREACHED = "BREACHED" # open past deadline, or accepted late
CLEAR = "CLEAR" # open, comfortably ahead of the deadline
@dataclass(frozen=True)
class Shipment:
isf_ref: str
load_port: str # UN/LOCODE, for logs
etl_local: datetime # tz-aware estimated time of lading
accepted_at: Optional[datetime] = None # tz-aware; None while open
def deadline_utc(self) -> datetime:
if self.etl_local.tzinfo is None:
raise ValueError(f"{self.isf_ref}: ETL must be timezone-aware")
return (self.etl_local - ISF_LEAD).astimezone(timezone.utc)
@dataclass(frozen=True)
class Assessment:
isf_ref: str
state: FilingState
deadline_utc: datetime
slack: timedelta # positive = time remaining; negative = overdue
exposure: Decimal # USD liquidated-damages exposure
class DeadlineMonitor:
"""Classifies open and closed ISF filings against the 24-hour rule."""
def __init__(self, warn_before: timedelta = timedelta(hours=6)):
self._warn_before = warn_before
def assess(self, ship: Shipment, now: Optional[datetime] = None) -> Assessment:
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
deadline = ship.deadline_utc()
# Closed filing: on-time only if accepted at or before the deadline.
if ship.accepted_at is not None:
accepted = ship.accepted_at.astimezone(timezone.utc)
slack = deadline - accepted
if accepted <= deadline:
return Assessment(ship.isf_ref, FilingState.ON_TIME,
deadline, slack, Decimal("0.00"))
logger.error(
"LATE ISF %s: accepted %s, deadline %s (over by %s)",
ship.isf_ref, accepted.isoformat(), deadline.isoformat(),
accepted - deadline,
)
return Assessment(ship.isf_ref, FilingState.BREACHED,
deadline, slack, MAX_LIQUIDATED_DAMAGES)
# Open filing: classify by remaining slack against the deadline.
slack = deadline - current
if slack <= timedelta(0):
logger.error(
"BREACHED open ISF %s at port %s: deadline %s passed %s ago",
ship.isf_ref, ship.load_port, deadline.isoformat(), -slack,
)
return Assessment(ship.isf_ref, FilingState.BREACHED,
deadline, slack, MAX_LIQUIDATED_DAMAGES)
if slack <= self._warn_before:
logger.warning(
"AT_RISK ISF %s: %s remaining before deadline %s",
ship.isf_ref, slack, deadline.isoformat(),
)
return Assessment(ship.isf_ref, FilingState.AT_RISK,
deadline, slack, Decimal("0.00"))
return Assessment(ship.isf_ref, FilingState.CLEAR,
deadline, slack, Decimal("0.00"))
def sweep(monitor: DeadlineMonitor, shipments: list[Shipment]) -> Decimal:
"""Assess a batch, alert on non-CLEAR states, return total exposure."""
total = Decimal("0.00")
for ship in shipments:
a = monitor.assess(ship)
total += a.exposure
if a.state in (FilingState.AT_RISK, FilingState.BREACHED):
logger.info(
"ALERT %s state=%s slack=%s exposure=USD %s",
a.isf_ref, a.state.value, a.slack, a.exposure,
)
return total
The design decision that matters: deadline_utc refuses a naive etl_local. The 24-hour rule is meaningless without a timezone, so the monitor makes the missing-timezone case a loud failure rather than a silent misclassification. Every downstream comparison then happens in UTC, which removes daylight-saving and cross-date ambiguity from the arithmetic entirely.
CLEAR through a six-hour AT_RISK warning window into the BREACHED zone, where each violation exposes the bond to up to USD 5,000.00.Verification steps
Run these checks before you let the monitor drive real alerts:
- Timezone integrity. Feed a shipment whose
etl_localis naive and assertdeadline_utcraises. Then feed the same wall-clock lading time taggedAsia/ShanghaiversusAmerica/Los_Angelesand confirm the two deadlines differ by the expected offset. A monitor that treats both identically will misfile near the boundary. - Boundary classification. Construct a filing accepted exactly at the deadline instant and assert
ON_TIME; move acceptance one second later and assertBREACHED. The comparison is inclusive of the deadline, matching CBP’s “no later than” language. - Warning-window trigger. Set
nowso the remaining slack is just inside the six-hour window and assertAT_RISK; step it back one minute and assertCLEAR. Confirm the warning fires exactly once per crossing, not on every sweep, if your alert sink is not idempotent. - Exposure accounting. Assert every
BREACHEDassessment reports exposure of exactly USD 5,000.00 and every other state reports USD 0.00. Sum a mixed batch throughsweepand confirm the total equals the breach count times the ceiling. - Daylight-saving crossing. Choose a load port and a lading date that straddle a DST transition and confirm the deadline lands on the correct absolute UTC instant. Doing the subtraction in local time before converting to UTC, as the code does, is what makes this correct.
- Clock injection. Pass an explicit
nowintoassessin tests rather than relying ondatetime.now. A monitor that reads the wall clock internally cannot be tested deterministically, and non-determinism here directly undermines the audit trail.
Edge cases & gotchas
- ETL drift. The estimated time of lading moves as the vessel schedule changes. A deadline computed at booking can be hours off by sailing. Re-assess on every ETL update, and treat a pull-forward that moves the deadline into the past as an immediate breach, not a warning.
- Rolled cargo. If cargo is rolled to a later vessel, the ISF may need to reference the new voyage even though the filing itself was timely. A timely original does not excuse a stale bill or voyage reference; reconcile the manifest match through the field mapping in Mapping bill of lading data to ISF 10+2 fields.
- Accepted is not transmitted. A transmission timestamp is when you sent the filing; the acceptance timestamp is when CBP acknowledged it. Only acceptance stops the clock. If your feed conflates the two, a filing that transmitted on time but was rejected and re-sent late will look compliant when it is not.
- Rejection restarts exposure. A rejected ISF is not an accepted ISF. When ACE returns an application-advice error, the clock keeps running while you correct it, so route rejections through ACE and ABI Rejection Handling immediately rather than treating the original transmission as protective.
- Mitigation depends on records. CBP may mitigate a liquidated-damages claim for a first offense or a good compliance history, but only if you can show the timeline. Persist the deadline, the acceptance timestamp, and the slack for every filing; the exposure figure the monitor records is your evidence, and a missing timestamp forfeits the argument.
- Bulk and break-bulk exemptions. The 24-hour lead applies to containerized ocean cargo; bulk and certain break-bulk shipments have different timing. Do not apply this monitor blindly to a bulk voyage — gate it on the shipment’s mode so you neither over-alert nor miss a genuinely different deadline.
Related
- Mapping bill of lading data to ISF 10+2 fields
- ISF 10+2 Data Assembly
- ACE and ABI Rejection Handling
- Compliance Reporting & ACE Transmission
Authoritative references: 19 CFR 149.2 (ISF timing); 19 CFR 113 (customs bonds and liquidated damages); CBP ACE ABI/ISF technical guidance; ISO 8601 timestamps; IANA time zone database.