Skip to main content

Table Rate Shipping

Overview​

Table Rate Shipping adds a WooCommerce shipping method whose cost is looked up in a table of bands. A table rate names the property being measured — price, weight or quantity — and holds rows saying what to charge between a minimum and a maximum value, optionally narrowed to a country, state or postcode.

Each row can carry both a fixed cost and a per-item cost, so a band can charge a base plus an amount for every item in it. A handling fee is added on top of whatever the table produced.

Rows are typed in or imported from CSV, which is how a carrier's published rate card usually arrives.

It is for stores whose postage is a published table rather than a rule.

Availability​

ItemValue
Module keytable-rate-shipping
TierPremium
Entitlement keytable-rate-shipping
Admin tabtable-rate-shipping
Enabled optionaiowc_module_enabled_table-rate-shipping (off until turned on)
REST namespaceaiowc/v1
Shipping methodRegistered through woocommerce_shipping_methods

Enabling the module creates the two tables below and seeds the defaults. registerHooks() returns early when the licence does not grant table-rate-shipping.

Settings​

Stored in the bundled option row aiowc_tr_settings.

Stored keyDefaultMeaning
enable_methodtrueWhether the shipping method is registered with WooCommerce.

With enable_method off the method is not registered at all, so it disappears from every zone; the REST routes still answer, which keeps the admin screen usable. Everything else about a rate — its bands, its scope, its fees — is table data rather than a setting.

Table rates​

A table rate is the method's configuration for one zone:

FieldMeaning
nameWhat the customer sees at checkout.
zone_idThe WooCommerce shipping zone it belongs to. Empty means every zone.
calculation_methodWhat the bands measure: price, weight or quantity. Defaults to price.
priorityWhich rate wins when more than one applies.
handling_feeAdded to whatever the matching band produced.
is_activeWhether the rate is offered.

Bands​

Each band row belongs to one table rate:

FieldMeaning
min_value / max_valueThe range of price, weight or quantity this row covers.
costThe fixed amount charged when the row matches.
per_item_costAn additional amount for each item.
country_code, state_code, postcodeNarrow the row to a destination. Empty matches anywhere.
labelAn optional label for the row.
sort_orderThe order rows are considered in.

CSV import​

POST /shipping/import-rates takes a CSV body and adds and updates the bands for one table rate. It deletes nothing unless replace_existing is sent true.

A header row is detected from its own contents, not assumed. A row counts as a header when at least one cell names a known column and no cell is numeric — the second condition is what protects a real band, since min, max and cost always hold numbers. When a header is present, columns are mapped by name, so reordering them is safe and an unrecognised column is ignored without shifting the others. Common aliases are accepted (min_value, per_item_cost, country_code, zip), so the module's own column names round-trip.

With no header, columns are read by position in this order:

min, max, cost, per_item, country, state, postcode, label

An empty cell becomes an empty value rather than a zero, except cost and per_item, which fall back to 0. Blank lines are skipped. A header carrying no cost column is refused rather than importing zeroes.

Valid rows import; invalid rows are reported and skipped, each error naming its line in the file, and counted in skipped_count. The response also carries updated_count, header_detected and replaced.

🔴 replace_existing: true deletes every existing band on that rate first. That is how the admin editor saves a whole card, and it is available to the import, but it is no longer what an import does by omission. One safety rule applies to it: a replace over a file containing any invalid row writes nothing at all, because applying the readable half would delete the bands the damaged half was meant to carry.

Admin screen​

The Table Rate Shipping tab lists the table rates, creates and edits them, clones one, and imports a CSV of bands into a chosen rate. A test tool posts a sample cart to /shipping/test-rate and shows which band matched and what it would charge.

REST API endpoints​

All routes are on aiowc/v1, answer with the { success, data, message } envelope, and require manage_woocommerce.

MethodPathPurposeRequired args
GET/shipping/table-ratesEvery table rate.–
POST/shipping/table-ratesCreate a table rate.name
PATCH / PUT /POST/shipping/table-rates/{id}Update a table rate. All three verbs registered.id
DELETE/shipping/table-rates/{id}Delete a table rate and its bands.id
POST/shipping/table-rates/{id}/cloneCopy a table rate, with its bands.id
POST/shipping/import-ratesImport a CSV of bands into a table rate.–
POST/shipping/test-ratePrice a sample cart against the tables.–

These share the /shipping/ prefix with Conditional Shipping Rules, but the two modules use different sub-paths — /shipping/table-rates and /shipping/rules — so neither shadows the other.

WooCommerce integration​

HookWhat the module does
woocommerce_shipping_initLoads the shipping method class.
woocommerce_shipping_methodsRegisters the method so it can be added to a zone.
woocommerce_update_options_shipping_*Saves the method's instance settings.

Database schema​

TableHolds
{prefix}aiowc_table_ratesThe rate itself: name, zone, what the bands measure, priority, handling fee, active flag.
{prefix}aiowc_rate_conditionsThe bands: min and max value, cost, per-item cost, destination narrowing, label, sort order.

Background jobs​

This module schedules no background jobs. Rates are read at checkout and written by an administrator; nothing needs to run on a timer.

Action hooks for integrators​

HookFired when
aiowc_track_eventThe module is enabled or disabled.

Entitlement limits​

table-rate-shipping is an on/off grant with no cap on the number of table rates or bands.

Health check​

The module reports a warning when its tables are missing or WooCommerce is inactive, and otherwise reports that it is functioning normally. A disabled module reports that it is disabled.

Known gaps​

  • There is no export, so a rate card that was imported cannot be downloaded back out.
  • A replace_existing import still has no confirmation step and no undo. It is opt-in rather than the default, which is the safeguard; there is no second one.

Changed on 2026-09-13​

Three CSV-import behaviours this page previously recorded are fixed. All three could corrupt or destroy a merchant's rate card without reporting anything:

  • The first row is no longer skipped unconditionally. A header is detected from its contents, so a file starting with real data keeps that row instead of silently losing it.
  • Columns are mapped by name when a header is present, so a reordered file no longer imports each value into the wrong field while passing validation.
  • 🔴 An import no longer replaces every band by default. It adds and updates; deleting the card requires replace_existing: true explicitly. Importing a partial CSV used to discard every band it did not contain.
  • A bad row no longer rejects the whole file: valid rows import and each invalid row is reported with its line number.