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.

Correction decision tree: routing a filed entry to PSC, 520(d), protest, or reconciliation A top-down decision cascade for correcting an entry already transmitted to CBP ACE. It begins with a filed entry summary that needs correction and runs three ordered gateways whose answers depend on liquidation state and elapsed time. Gateway one asks whether the entry has liquidated. If yes, the only remaining channel is a protest under 19 USC 1514, which must be filed within 180 days of the liquidation date, shown as a terminal on the right. If not yet liquidated, the cascade continues. Gateway two asks whether the correction is a post-importation claim for preferential free-trade-agreement treatment. If yes, it travels as a 520(d) claim with a one-year deadline from the date of importation, shown as a second right-hand terminal. If no, the cascade continues. Gateway three asks whether the entry is still inside the Post-Summary Correction window, up to 300 days from entry and before liquidation. If yes, a PSC is filed in ACE and the corrected CBP 7501 is re-transmitted, shown as a third right-hand terminal. If the window has closed, the bottom terminal indicates that no correction avenue remains except awaiting liquidation and protesting. A footnote notes that reconciliation is a separate pre-declared channel that trues up entries flagged at filing for value, classification, or 520(d) preference. Decision gateways are shaded gold, successful correction terminals are shaded teal, and the closed-window terminal is shaded red. yes no · pre-liquidation yes no · duty/value fix yes window closed Filed entry needs correction Has the entry liquidated? Post-import FTA preference claim? Inside PSC window (≤300 days)? Protest · 19 USC 1514 within 180 days of liquidation 520(d) claim within 1 year of importation PSC in ACE re-transmit corrected CBP 7501 No PSC avenue remains await liquidation, then protest Reconciliation is a separate pre-declared channel: entries flagged at filing true up value, classification, or 520(d) later. ordered gateway correction channel window closed
The correction channel is decided by liquidation state and elapsed days, not by the nature of the error. PSC serves pre-liquidation corrections up to 300 days from entry; 520(d) carries post-importation preference claims for a year; a protest is the only remedy once the entry liquidates, and it expires 180 days later.

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:

ΔD=(Vcrc+Sc)(Voro+So)\Delta D = (V_c \cdot r_c + S_c) - (V_o \cdot r_o + S_o)

where VV is entered value, rr the ad valorem rate, and SS the extended specific duty, with subscripts oo and cc marking the original and corrected positions. The most common amendment — a reclassification that moves a line from one HTS subheading to another — changes rr, and often VV 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_hash key, 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.

Up: Compliance Reporting & ACE Transmission