DEV Community

InvoiceTariff
InvoiceTariff

Posted on

How Much Tariff Do You Actually Pay Importing From China in 2026? A Code-Level Breakdown

If you buy goods from China and import them into the United States, the number on your customs entry is never "the tariff rate." It's a stack: a base MFN duty, plus Section 301 layers, plus Section 232 if your product touches steel or aluminum, plus user fees that often cost more than the duties. Most sellers discover this when the invoice arrives.

I maintain a small open tariff engine, so instead of explaining this with vibes, I'll walk through the actual math our engine performs — with real numbers pulled straight from the versioned ruleset JSON (v0.3.2, as of 2026-09-24, FY2027 fee table in effect since 2026-10-01) — and give you a runnable TypeScript (plus Python) implementation you can adapt.

By the end you'll be able to answer, to the dollar: what will this shipment actually cost me in duties and fees?

The 2026 stack, layer by layer

For a typical China-origin shipment the stack currently looks like this:

Duty = MFN(base rate)                    ← USITC Harmonized Tariff Schedule
     + Section 301 (China lists 1–4A)    ← USTR actions, per HTS code
     + Section 301 (2026 global program) ← country tiers, effective 2026-07-24
     + Section 232 (steel/aluminum/...)  ← only for covered products
     ─ IEEPA "fentanyl tariff" (10%)     ← TERMINATED 2026-02-24 (SCOTUS); historical layer
Fees = MPF  0.3464% of value, min $34.58, max $670.86   (FY2027, since 2026-10-01)
     + HMF  0.125% of value              ← ocean shipments only
Enter fullscreen mode Exit fullscreen mode

A few things worth calling out, because they trip people up:

  • MFN is per-code, and "Free" is a real answer. Laptops (HTS 8471.30) have a Free MFN general rate in the USITC schedule. That does not mean the laptop enters free in 2026 — see below.
  • The China Section 301 lists are per-code too. A cotton T-shirt (61091000) sits on List 4A at 7.5%; a laptop isn't on any of the 17 list lines in the current ruleset.
  • There is now a global Section 301 program (effective 2026-07-24) with country tiers — China sits in the 12.5% tier. This is new in 2026 and a lot of landed-cost spreadsheets don't know it exists yet.
  • IEEPA is a refund question now, not a cost question. The 10% "fentanyl tariff" layer was struck down on 2026-02-24. If you paid it, that money may be recoverable — that's a lookup problem, not a stacking problem.
  • The fees are not rounding errors. MPF is capped at $670.86, so above ~$194k of declared value it stops growing, but in the $10k–$190k range it's real money. And note the floor just moved: the FY2026 minimum was $33.58, FY2027 (in effect since 2026-10-01) is $34.58.

The data model (this is the part developers care about)

You can't compute this stack without a versioned dataset, because every layer changes on its own schedule. Our engine reads one JSON ruleset; here are the exact rows we need for two worked examples, copied verbatim from ruleset-seed.json v0.3.2 (asOf 2026-09-24):

// MFN rows (engine/data/ruleset-seed.json → .mfn[])
// ⚠️ v0.3.x rows carry sample/unverified flags upstream — estimates, not customs-certified
{ "hts8": "84713001", "description": "Portable automatic data processing machines…", "general": "Free",  "sample": true }
{ "hts8": "61091000", "description": "T-shirts, singlets… of cotton",                "general": "16.5%", "sample": true }

// China Section 301 (→ .china301[]) — 17 lines total
{ "hts8": "61091000", "list": "4A", "rate": 7.5,
  "legalBasis": "Trade Act 1974 §301 (USTR action, 9903.88.01 series)" }

// Global Section 301 (→ .global301, effectiveFrom 2026-07-24) — 60 country tiers
{ "country": "CN", "rate": 12.5,
  "legalBasis": "Trade Act 1974 §301 (global forced-labor program; FR 2026-15181 §1(a)(i))" }

// Fee table (→ .fees[]) — fiscal-year keyed; the FY2027 row is in effect since 2026-10-01
{ "fiscalYear": 2027, "validFrom": "2026-10-01", "validTo": "2027-09-30",
  "mpfRate": 0.3464, "mpfMin": 34.58, "mpfMax": 670.86, "hmfRate": 0.125,
  "informalTiers": [ { "maxValue": 100, "fee": 2.69 },
                     { "maxValue": 1000, "fee": 8.06 },
                     { "maxValue": 2500, "fee": 12.09 } ] }
Enter fullscreen mode Exit fullscreen mode

Note what the data gives you for free: legalBasis and source on every line. When a client asks "where did 12.5% come from?", the answer is a Federal Register citation, not a shrug.

The TypeScript

This is a simplified but runnable version of the stacking logic — no exclusions, no 232 matching, no informal-entry tiers, just the main path:

// Data: InvoiceTariff ruleset v0.3.2 (engine/data/ruleset-seed.json, asOf 2026-09-24)
// Fee table: FY2027, in effect 2026-10-01
const MFN_GENERAL: Record<string, number> = {
  "84713001": 0,     // "Free"   (laptops)
  "61091000": 16.5,  // cotton T-shirts
};

const CHINA_301_LISTS: Record<string, number> = {
  "61091000": 7.5,   // List 4A
  // 84713001: not on any China 301 list in v0.3.2
};

const GLOBAL_301_TIER_CN = 12.5;      // effective 2026-07-24 (FR 2026-15181)
const FEES_FY2027 = {
  mpfRate: 0.3464, mpfMin: 34.58, mpfMax: 670.86,
  hmfRate: 0.125,                     // ocean only
};

function pct(rate: number, value: number) {
  return (rate / 100) * value;
}

export function stackedTariff(opts: {
  hts8: string;
  declaredValue: number;   // USD, transaction value
  origin: string;          // "CN" in this example
  mode: "ocean" | "air";
}) {
  const { hts8, declaredValue, mode } = opts;

  const mfn = pct(MFN_GENERAL[hts8] ?? 0, declaredValue);
  const list301 = pct(CHINA_301_LISTS[hts8] ?? 0, declaredValue);
  const global301 = opts.origin === "CN" ? pct(GLOBAL_301_TIER_CN, declaredValue) : 0;

  const mpf = Math.min(
    Math.max(pct(FEES_FY2027.mpfRate, declaredValue), FEES_FY2027.mpfMin),
    FEES_FY2027.mpfMax
  );
  const hmf = mode === "ocean" ? pct(FEES_FY2027.hmfRate, declaredValue) : 0;

  const duty = mfn + list301 + global301;
  return {
    lines: { mfn, list301, global301, mpf, hmf },
    total: duty + mpf + hmf,
    effectiveRatePct: +(((duty + mpf + hmf) / declaredValue) * 100).toFixed(2),
  };
}

// Worked example 1: $50,000 of laptops from China, ocean freight
console.log(stackedTariff({ hts8: "84713001", declaredValue: 50_000, origin: "CN", mode: "ocean" }));
// → mfn 0, list301 0, global301 6250, mpf 173.20, hmf 62.50
// → total $6,485.70  (effective 12.97%)

// Worked example 2: $20,000 of cotton T-shirts from China, ocean
console.log(stackedTariff({ hts8: "61091000", declaredValue: 20_000, origin: "CN", mode: "ocean" }));
// → mfn 3300, list301 1500, global301 2500, mpf 69.28, hmf 25
// → total $7,394.28  (effective 36.97%)
Enter fullscreen mode Exit fullscreen mode

And the same thing in Python, if that's your stack:

# Data: InvoiceTariff ruleset v0.3.2 (ruleset-seed.json, asOf 2026-09-24)
# Fee table: FY2027, in effect 2026-10-01
MFN = {"84713001": 0.0, "61091000": 16.5}
CHINA_301 = {"61091000": 7.5}
GLOBAL_301_CN = 12.5          # effective 2026-07-24
MPF = dict(rate=0.3464, lo=34.58, hi=670.86)
HMF_OCEAN = 0.125

def stacked(hts8: str, value: float, origin: str = "CN", ocean: bool = True) -> dict:
    mfn        = MFN.get(hts8, 0.0) / 100 * value
    list_301   = CHINA_301.get(hts8, 0.0) / 100 * value
    global_301 = GLOBAL_301_CN / 100 * value if origin == "CN" else 0.0
    mpf        = min(max(MPF["rate"] / 100 * value, MPF["lo"]), MPF["hi"])
    hmf        = HMF_OCEAN / 100 * value if ocean else 0.0
    total      = mfn + list_301 + global_301 + mpf + hmf
    return {"mfn": mfn, "list301": list_301, "global301": global_301,
            "mpf": mpf, "hmf": hmf, "total": total,
            "effective_pct": round(total / value * 100, 2)}

print(stacked("84713001", 50_000))  # total ≈ 6485.70 → 12.97%
print(stacked("61091000", 20_000))  # total ≈ 7394.28 → 36.97%
Enter fullscreen mode Exit fullscreen mode

Read those two outputs again, because they contain the whole story of 2026 sourcing math:

  • The laptop went from effectively duty-free to ~13% the moment the global program went live on 2026-07-24 — with zero change to the HTS schedule.
  • The T-shirt is now pushing 37% all-in between MFN, List 4A, and the global tier. If your pricing model still assumes "MFN + 7.5%", you're underquoting by 18 points.

The parts I left out (on purpose)

The snippet above is the happy path. The production engine adds:

  • Section 232 measures — 8 lines in the ruleset (steel/aluminium articles at 50% since 2025-06-04, plus derivatives). Product-level matching, not code-level.
  • Exclusion windows — both China-list exclusions (with expiry dates) and 39 HTS-prefix exclusions under the global program.
  • Informal entry tiers — shipments under $2,500 pay flat fees ($2.69/$8.06/$12.09) instead of MPF/HMF, which completely changes the small-parcel math, together with the de minimis threshold rules.
  • The caveat section of the ruleset itself: v0.3.x flags most non-fee rows as sample/unverified upstream, and unverifiedNotes records that several exemption lists published as PDF attachments aren't machine-encoded yet — meaning for those goods the engine can over-count the global 301 layer. Estimates first; your customs broker still signs the entry.

If you want to sanity-check a code against the full 5,689-row HTS table, the free tariff calculator runs exactly this stack in the browser and shows the per-line provenance — for example the HTS 8471.30 tariff rate page shows the Free MFN row and every layer stacked on top of it. For the 301 layer specifically, the China tariff guide walks the lists line by line.

The layer you should be watching: change velocity

The nasty part of 2026 isn't any single rate — it's that the stack changes on independent calendars. The ruleset's nextHardDates had three entries when this snapshot was taken; two have already fired:

  • 2026-09-29 — Section 232 pharmaceuticals expansion ✅ (took effect)
  • 2026-10-01 — FY2027 MPF reset ✅ (took effect; the fee constants in this post are already FY2027: min $34.58 / max $670.86)
  • 2027-01-10 — China exclusions + tariff truce + port fee measures now share this extended deadline (extension announced 2026-09-23, FR notice pending)

You can't diff this by hand. Our ruleset ships a machine-readable changelog.json (51 entries and counting), which also feeds the public tariff change radar — it's the cheap way to find out your cost basis moved before the invoice tells you.

Takeaways

  1. Model the stack, not "the tariff rate": MFN + 301 layers + fees, each with its own effective dates.
  2. Keep rates in versioned data, not in your code — the logic barely changes, the data changes monthly.
  3. Track legalBasis/source per line; "because the spreadsheet said so" doesn't survive a customs audit.
  4. Fee tables have fiscal years. FY2027 MPF (min $34.58 / max $670.86) went live 2026-10-01 — diary the next rollover and the 2027-01-10 China measures cluster.
  5. If you paid the IEEPA tariff before February 2026, start reading up on refund channels instead of writing it off.

All rates in this post come from a single versioned snapshot (InvoiceTariff ruleset v0.3.2, as of 2026-09-24) and the FY2027 fee table — they are estimates for planning, not legal advice. Your customs entry is signed by your broker, not by your code.

Questions, corrections, or your own worked examples are welcome in the comments.

Top comments (0)