Post-Entry Amendment Workflows
A post-entry amendment workflow is the controlled process that corrects a customs entry already transmitted to CBP — routing each correction to the right legal instrument, computing the exact duty delta in Decimal, and recording the change on an immutable ledger so the amended filing survives audit. Once an entry summary leaves the broker’s system and lands in ACE, it is no longer a draft: it is a legal declaration of value, classification, and duty owed. Correcting it is not an edit — it is a distinct regulatory act whose available channel depends entirely on where the entry sits in its lifecycle. Part of the Compliance Reporting & ACE Transmission architecture, the amendment layer sits downstream of the original filing and consumes the same classified line items, but it operates under a different clock and a stricter evidentiary standard. For the trade-compliance officer, the choice between a Post-Summary Correction, a protest, and a reconciliation entry is a choice of statute; for the Python ETL team, that choice must be a deterministic function of the entry’s liquidation status and dates, never a manual judgment made in a spreadsheet.
Problem Framing: The Wrong Instrument at the Wrong Time
The failure mode this workflow exists to prevent is filing the right correction through the wrong channel — or through the right channel after its window has closed. A filed entry moves through a fixed sequence of states, and each state exposes a different corrective avenue. Miss the boundary and the correction is either rejected outright or, worse, accepted into a channel that cannot deliver the refund or the liability adjustment the importer actually needs.
Three defects recur in hand-built amendment logic. First, window blindness: a correction is prepared as a Post-Summary Correction (PSC) when the entry has already liquidated, so the only surviving remedy is a protest under 19 USC 1514 — but the 180-day protest clock has quietly started ticking from the liquidation date, and nobody is counting it. Second, channel confusion: a post-importation claim for preferential tariff treatment under a free-trade agreement is jammed into a PSC when it should travel as a 520(d) claim, which carries its own one-year deadline and its own evidentiary package; the PSC is accepted, the preference is denied, and the duty overpayment is never recovered. Third, delta drift: the additional duty owed or the refund claimed is recomputed with floating-point arithmetic against a re-fetched rate table, so the amended figure disagrees with the original assessment by a cent, and the re-transmitted entry fails ACE’s decimal-precision validation.
Each defect turns a routine correction into a compliance exposure. A late protest forfeits a legitimate refund permanently. A misrouted 520(d) claim forfeits a preferential rate. A drifting delta triggers an ABI rejection, and repeated rejections draw the kind of CBP scrutiny that a Focused Assessment is built on. The workflow below closes those gaps by making the channel a pure function of liquidation state and dates, computing every delta in exact decimal arithmetic, and binding each amendment to an append-only ledger that names the instrument, the reason, and the prior state it superseded.
Schema / Data Contract
An amendment is only defensible if the record of it is complete, and completeness begins with the contract that shapes every correction event before it is persisted. Each amendment must carry the entry it corrects, the specific line and field it touches, the instrument it travels under, the reason code that justifies it, the before and after values, the computed duty delta as an exact decimal, and a link to the prior state it supersedes. The dataclass below formalizes that surface, and the enums pin the amendment type and disposition to a closed vocabulary so a free-text channel can never leak into the ledger.
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import date, datetime, timezone
from decimal import Decimal
from enum import Enum
from typing import Optional
class AmendmentType(str, Enum):
PSC = "PSC" # Post-Summary Correction, pre-liquidation
PROTEST_1514 = "PROTEST" # 19 USC 1514, post-liquidation
CLAIM_520D = "520D" # post-importation FTA preference claim
RECONCILIATION = "RECON" # pre-declared recon true-up
class AmendmentStatus(str, Enum):
DRAFT = "DRAFT"
TRANSMITTED = "TRANSMITTED"
ACCEPTED = "ACCEPTED"
REJECTED = "REJECTED"
SUPERSEDED = "SUPERSEDED"
@dataclass(frozen=True)
class AmendmentEvent:
entry_number: str # 3-char filer code + 8 digits (11 total)
line_number: int
amendment_type: AmendmentType
reason_code: str # controlled CBP reason vocabulary
field_changed: str # e.g. "hts_code", "entered_value", "duty_rate"
old_value: str
new_value: str
duty_delta: Decimal # signed: positive owed, negative refund
entry_date: date
liquidation_date: Optional[date]
filed_by: str
prior_hash: Optional[str] # hash of the state this event supersedes
payload_hash: str = "" # computed at seal time
filed_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
status: AmendmentStatus = AmendmentStatus.DRAFT
The duty_delta field is a signed Decimal, never a float: a positive value is additional duty the importer must tender, a negative value is a refund the amendment claims. Holding it as Decimal is not stylistic — it is what keeps the re-transmitted figure byte-identical to the arithmetic ACE will re-run on receipt. The prior_hash field is what makes the record a chain rather than a set: every amendment names the exact state it replaced, so a verification can walk backward from the current position to the original entry without ambiguity.
The persistence layer mirrors that intent. The amendment ledger is an append-only table: corrections are inserted, never updated in place, and a superseded row keeps its content while a new row records the change. The DDL below enforces the append-only property at the database boundary with a trigger, so no application bug and no ad-hoc query can rewrite history.
CREATE TABLE amendment_ledger (
amendment_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
entry_number CHAR(11) NOT NULL,
line_number INTEGER NOT NULL,
amendment_type TEXT NOT NULL
CHECK (amendment_type IN ('PSC','PROTEST','520D','RECON')),
reason_code TEXT NOT NULL,
field_changed TEXT NOT NULL,
old_value TEXT NOT NULL,
new_value TEXT NOT NULL,
duty_delta NUMERIC(14,2) NOT NULL, -- exact cents, signed
entry_date DATE NOT NULL,
liquidation_date DATE,
filed_by TEXT NOT NULL,
prior_hash TEXT,
payload_hash TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'DRAFT',
filed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Fast lookup of the current amendment chain for one entry line.
CREATE INDEX idx_amendment_entry_line
ON amendment_ledger (entry_number, line_number, filed_at DESC);
-- Enforce append-only: block any UPDATE or DELETE at the boundary.
CREATE OR REPLACE FUNCTION amendment_ledger_immutable()
RETURNS TRIGGER AS $BODY$
BEGIN
RAISE EXCEPTION 'amendment_ledger is append-only; % is not permitted', TG_OP;
END;
$BODY$ LANGUAGE plpgsql;
CREATE TRIGGER trg_amendment_ledger_immutable
BEFORE UPDATE OR DELETE ON amendment_ledger
FOR EACH ROW EXECUTE FUNCTION amendment_ledger_immutable();
NUMERIC(14,2) stores duty deltas as exact cents; it never rounds behind your back the way a binary float column would. The immutability trigger is the structural guarantee behind every compliance claim later in this reference: the ledger can only grow, so the audit trail of amendments is not a policy the application promises to honor — it is a property the database refuses to violate.
Step-by-Step Implementation
The workflow runs in four deterministic stages: resolve the correction channel from state and dates, compute the exact duty delta, seal the event to the append-only ledger, and re-transmit through the appropriate ACE message. Each stage is a pure function of its inputs, and each records enough to reconstruct the decision.
Stage 1 — Resolve the channel from state and dates
The channel is never chosen by hand. It falls out of the entry’s liquidation status and two elapsed-time windows, applied in strict precedence: liquidation first, then post-importation preference, then the PSC window.
import logging
from datetime import date, timedelta
logging.basicConfig(
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s"
)
logger = logging.getLogger("amendment.router")
PSC_WINDOW_DAYS = 300 # from entry date, and before liquidation
PROTEST_WINDOW_DAYS = 180 # from liquidation date
CLAIM_520D_WINDOW_DAYS = 365 # from date of importation
class ChannelClosed(Exception):
"""No corrective instrument is available for the entry's current state."""
def resolve_channel(
*,
entry_date: date,
import_date: date,
liquidation_date: date | None,
is_preference_claim: bool,
today: date,
) -> AmendmentType:
"""Deterministically select the amendment instrument. Order is load-bearing."""
if liquidation_date is not None:
deadline = liquidation_date + timedelta(days=PROTEST_WINDOW_DAYS)
if today <= deadline:
return AmendmentType.PROTEST_1514
raise ChannelClosed(f"protest window closed on {deadline.isoformat()}")
if is_preference_claim:
deadline = import_date + timedelta(days=CLAIM_520D_WINDOW_DAYS)
if today <= deadline:
return AmendmentType.CLAIM_520D
raise ChannelClosed(f"520(d) window closed on {deadline.isoformat()}")
deadline = entry_date + timedelta(days=PSC_WINDOW_DAYS)
if today <= deadline:
return AmendmentType.PSC
raise ChannelClosed(f"PSC window closed on {deadline.isoformat()}")
The precedence is not interchangeable. Liquidation is tested first because once an entry liquidates, the PSC channel is gone regardless of how many days remain on the 300-day count — liquidation, not the calendar, closes it. The preference claim is tested before the PSC window because a post-importation free-trade claim is a distinct instrument with its own deadline, not a generic value correction. Only when neither of those applies does the workflow reach for a PSC. A state with no open window raises ChannelClosed rather than defaulting to any channel, because there is no safe default: an amendment filed through a closed instrument is worse than no amendment at all.
Stage 2 — Compute the duty delta in exact decimal
The heart of every amendment is the difference between what was assessed and what should have been. That difference must be computed in Decimal with an explicit rounding mode, because it will be tendered to or claimed from the U.S. Treasury and re-validated by ACE to the cent.
from decimal import Decimal, ROUND_HALF_UP
CENTS = Decimal("0.01")
def _money(value: Decimal) -> Decimal:
"""Quantize to cents with CBP-consistent half-up rounding."""
return value.quantize(CENTS, rounding=ROUND_HALF_UP)
@dataclass(frozen=True)
class DutyPosition:
entered_value: Decimal # customs value in USD
ad_valorem_rate: Decimal # fraction, e.g. Decimal("0.025")
specific_duty: Decimal = Decimal("0") # extended per-unit duty
def assessed_duty(self) -> Decimal:
ad_valorem = self.entered_value * self.ad_valorem_rate
return _money(ad_valorem + self.specific_duty)
def compute_duty_delta(original: DutyPosition, corrected: DutyPosition) -> Decimal:
"""Signed delta: positive means duty owed, negative means refund due."""
delta = corrected.assessed_duty() - original.assessed_duty()
signed = _money(delta)
logger.info(
"duty delta computed | original=%s corrected=%s delta=%s",
original.assessed_duty(), corrected.assessed_duty(), signed,
)
return signed
The compound-duty structure — an ad valorem component on the entered value plus an extended specific component — mirrors the assessment logic in the Duty Formula Calculation Frameworks, and it is deliberately the same arithmetic run twice: once on the original position, once on the corrected one. Reusing one function for both sides is what guarantees the delta is internally consistent. In formula terms the signed delta is:
where is entered value, the ad valorem rate, and the extended specific duty, with subscripts and marking the original and corrected positions. The most common amendment — a reclassification that moves a line from one HTS subheading to another — changes , and often if the correction also revises the declared value; the delta captures both movements in a single figure the ledger stores as NUMERIC(14,2).
Stage 3 — Seal the event to the append-only ledger
With the channel resolved and the delta computed, the event is sealed: its payload is hashed together with the prior state’s hash, chaining it to everything that came before, and inserted. Nothing is ever updated — a supersession is a new row plus a status flip on the old one, and even that flip is an insert in a status-history projection, never a rewrite of the sealed content.
import hashlib
import json
from dataclasses import replace
def seal_event(event: AmendmentEvent, prior_hash: str | None) -> AmendmentEvent:
"""Chain the event to prior state and freeze its payload hash."""
material = json.dumps(
{
"entry_number": event.entry_number,
"line_number": event.line_number,
"amendment_type": event.amendment_type.value,
"field_changed": event.field_changed,
"old_value": event.old_value,
"new_value": event.new_value,
"duty_delta": str(event.duty_delta),
"filed_at": event.filed_at.isoformat(),
"prior_hash": prior_hash or "GENESIS",
},
sort_keys=True,
separators=(",", ":"),
)
digest = hashlib.sha256(material.encode("utf-8")).hexdigest()
return replace(event, prior_hash=prior_hash, payload_hash=digest)
The hash is computed over a canonical JSON encoding — sorted keys, fixed separators — so the same logical event always produces the same digest regardless of dictionary ordering. duty_delta is serialized with str(...) rather than as a JSON number, preserving the exact decimal string and keeping the hash stable across platforms that would otherwise render the value differently. The chain of prior_hash links is what lets a verification prove that the amendment history has not been reordered or backdated: break any link and every downstream digest changes.
Stage 4 — Re-transmit through the correct ACE message
Finally, the sealed amendment becomes a re-transmission. A PSC re-sends a corrected entry summary — the same CBP 7501 line structure produced by the Entry Summary Filing Schemas, now carrying the amended figures and a PSC reason code. A 520(d) claim and a protest travel through their own message types with supporting documentation attached. In every case the re-transmission carries the payload_hash as its idempotency key, so a retried send after a network failure is recognized as the same amendment rather than filed twice — the same idempotency discipline used when resubmitting rejected ABI entries.
Validation & Determinism
Determinism is what an auditor tests, and an amendment workflow has two things it must reproduce exactly: the channel decision and the duty delta. Both are guaranteed by construction. The channel is a pure function of entry_date, import_date, liquidation_date, and the preference flag against a fixed today; replay the same four inputs and the same instrument comes back every time, including the ChannelClosed outcome. Passing today as an explicit argument rather than reading the clock inside the router is what makes the decision testable and replayable — a determination made on the entry’s amendment date can be reconstructed months later by supplying that same date.
The duty delta is deterministic because every monetary value is a Decimal quantized to cents with a single, declared rounding mode, and because the original and corrected positions run through one identical assessed_duty method. There is no path where the two sides of the subtraction are computed differently. The re-transmitted figure therefore matches ACE’s own re-computation to the cent, clearing the same decimal-precision validation described under validating entered value with ACE decimal precision. Where a corrected HTS code cannot be resolved to a rate at all — a reclassification into a subheading the schedule has not yet ingested — the delta cannot be computed deterministically, and the record is quarantined for review rather than amended against a guessed rate, using the same broker-review routing that Fallback Routing for Unmapped Codes applies to entries whose classification lineage cannot be reconstructed. The append-only ledger closes the loop: because rows are immutable and hash-chained, a replay that produced a different delta would produce a different digest, and the mismatch would be visible immediately rather than buried.
Downstream Integration
An amendment is not a terminal event — it re-enters the transmission architecture as a new declaration. A sealed PSC feeds directly back into the Entry Summary Filing Schemas, which reassemble the corrected CBP 7501 line items from the amended figures and hand them to the ACE transmission layer. The amended entry then passes through the same acceptance path as an original filing, and if ACE returns an application-advice error, the amendment is subject to the same ACE and ABI rejection handling as any other transmission — a rejected PSC is decoded, corrected, and resubmitted under its idempotency key.
The amendment layer also closes a loop back to the classification architecture upstream. Many corrections originate as reclassifications: a broker or a CBP ruling determines that a line was entered under the wrong HTS subheading, and the corrected code changes the rate, the delta, and sometimes the preferential eligibility. Those reclassifications flow from the classification and duty engines in the Core Architecture & Tariff Mapping reference, and when the original entry was itself the product of a fallback — a code the pipeline could not confidently map at filing time and routed through Fallback Routing for Unmapped Codes — the amendment is the mechanism that trues up the provisional classification once the correct code is known. The refund arithmetic that follows a reclassification is detailed enough to warrant its own guide, reconciling duty refunds after reclassification.
Scaling & Resilience
Amendment volume is bursty in a way original filings are not. A single CBP ruling, a retroactive rate change, or a supplier’s corrected invoice can trigger thousands of PSCs against entries filed over months, all needing channel resolution, delta computation, and re-transmission inside a narrow window before the earliest entries liquidate. Four controls keep the workflow inside its budget under that load:
- Batched channel resolution — the router is pure and cheap, so an entire cohort of entries is resolved in one vectorized pass over their dates, partitioning the batch into PSC, 520(d), protest, and closed-window buckets before any I/O.
- Idempotent re-transmission — because every amendment carries a stable
payload_hashkey, a burst that overwhelms the ACE gateway can be retried wholesale without risk of double-filing; the gateway dedupes on the key. - Bounded concurrency — re-transmissions dispatch through a semaphore-capped async queue against a pooled ACE connection, so a mass amendment event cannot exhaust the same connection pool the original filing path depends on.
- Append-only writes — the ledger only ever inserts, so amendment writes never contend on row locks with concurrent readers; the immutability trigger adds no write-path cost beyond a constant-time check.
For resilience, a re-transmission that fails mid-flight is safe to replay because the ledger entry was sealed before the send: the durable record of intent exists independently of whether the ACE acknowledgment arrived. Transient gateway failures are retried with exponential backoff; a persistent failure trips a circuit breaker that pauses re-transmission and alerts a broker rather than hammering a degraded endpoint. The most important resilience property, though, is temporal: the batched resolver surfaces entries approaching a window boundary first, so the cohort most at risk of aging out of its PSC window is transmitted before the cohort with weeks to spare. A correction that misses its window is not a retryable error — it is a permanently forfeited remedy — so the scheduler treats the deadline, not the queue position, as the ordering key.
Compliance Obligations & Retention
Every amendment is evidence, and the ledger is built to be produced intact under a Focused Assessment. Each sealed row carries the instrument it traveled under, the reason code, the before-and-after values, the signed duty delta, the filer, the timestamp, and the hash chain linking it to the state it superseded — the complete provenance of a correction, immutable from the moment of the insert. Retention follows the CBP recordkeeping horizon: amendment records are held for at least five years past the later of the entry date or the final liquidation, on an immutable storage tier, so a correction filed years ago can be answered from the primary record rather than reconstructed.
The instrument boundaries are themselves compliance obligations, not conveniences. A PSC must be filed before liquidation and no later than 300 days from entry; a protest under 19 USC 1514 must be filed within 180 days of the liquidation it contests; a post-importation preference claim under 520(d) must be filed within one year of importation and carry the supporting certification the agreement requires. The router encodes those deadlines as the law states them and refuses to file through a closed one, which is the single most consequential compliance control in the workflow: it makes missing a statutory deadline a loud ChannelClosed exception at decision time rather than a silent rejection weeks later. Corrections that increase duty owed carry a distinct obligation from those that claim refunds — an under-declaration surfaced by the importer through a timely PSC is a materially different posture before CBP than the same error found on audit — and the reason code that accompanies each amendment records which it is. Any amendment the workflow cannot resolve deterministically — a closed window, an unmapped corrected code, an ambiguous liquidation status — is escalated to a licensed broker through a human-in-the-loop gate rather than auto-filed. Immutable provenance, statutory deadline enforcement, exact decimal deltas, and mandatory escalation are what make a post-entry correction defensible instead of merely transmitted.
Authoritative references: 19 USC 1514 (protests); 19 CFR Part 174 (protest procedures); 19 USC 1520(d) (post-importation preference claims); CBP ACE Post-Summary Correction (PSC) business rules; 19 CFR Part 163 (recordkeeping). Consult current CBP guidance and a licensed customs broker before filing.
Related
- Filing post-summary corrections with Python — the full PSC assembly and ACE re-transmission implementation, reason codes, and idempotency keys
- Reconciling duty refunds after reclassification — computing and reconciling refund positions when a corrected HTS code changes the rate
- Entry Summary Filing Schemas — reassembles the corrected CBP 7501 line items an amendment re-transmits
- ACE and ABI Rejection Handling — decodes and idempotently resubmits an amendment ACE rejects
- Duty Formula Calculation Frameworks — the compound-duty arithmetic reused on both sides of every delta