Shipping rates
A shipping method is the line a buyer picks in the checkout, and it owns the price. The delivery promise — tracking template, cut-off, handling and transit days — is inherited from the carrier wherever the method states none of its own.
Base path: https://api.revenexx.com/v1/shipping.
Methods
curl -X POST https://api.revenexx.com/v1/shipping/methods \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{
"code": "standard",
"name": "Standard delivery",
"pricing_type": "fixed",
"price": 4.90,
"currency": "EUR",
"countries": ["DE", "AT"],
"free_above": 250,
"carrier_id": "…",
"tax_class": "standard",
"enabled": true
}'
| Field | Meaning |
|---|---|
code | Required. What a checkout and an order line store — the value every integration joins on. |
pricing_type | fixed, free or matrix. |
price, currency | The fixed price. |
matrix_basis | For matrix: weight, quantity, order_value or attribute. |
matrix_attribute | The named product attribute, when the basis is attribute. |
countries | Allow-list. Empty or null is unrestricted. |
free_above | Free-shipping threshold. |
quote_above | Above this, the method quotes rather than prices. |
carrier_id / carrier | The carrier it ships with. |
tax_class | A tax class code from the market. |
eta_days_min, eta_days_max | Override the carrier's transit promise. |
enabled | Defaults to false. |
checkout_notes | A map of locale code to the note text the checkout prints under the method. Null means no note is printed. Every rate in POST /v1/shipping/rates returns the method's checkout_notes too, so a checkout can print it. |
position | The order the checkout prints them in. |
A new method is quoted by nobody until two things are true: it is enabled, and — for a matrix method — it has tiers. Until then it appears in excluded with "matrix has no rate tiers configured" rather than in the rates.
carrier_id and the legacy carrier code are both accepted, and neither is verified against the carrier table here: an unmatched code becomes a plain carrier name on the rate, not an error.
Read and remove one method
GET /v1/shipping/methods/{id} reads the configuration of one method by row id. It is not a price. A matrix method's prices are its tiers, and the price for a basket comes from POST /v1/shipping/rates. For a matrix method, price is whatever the column holds (0 unless someone set it), not what the method charges.
DELETE /v1/shipping/methods/{id} is a hard delete, and it never answers a conflict, even for a method that checkouts still offer. It deletes every rate tier of the method with it, and the table cannot be recovered afterwards. The answer is {"deleted": true, "id": "…"}. For a method a checkout may still hold in a session, enabled: false is the safer edit.
Status codes for both: 400 when the id is not a uuid, and 404 when there is no method with that id in this tenant.
Matrix tiers
A tier is one row of a matrix method's price table: a from_value threshold and the price charged at or above it.
curl -X POST "https://api.revenexx.com/v1/shipping/methods/$METHOD_ID/tiers" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"from_value":10,"price":8.90}'
The bound is inclusive, and the winning tier is the one with the highest from_value at or below the measured value — so a measure of exactly 10 is priced by the tier at 10. The last tier has no upper bound.
What the number measures is the method's matrix_basis: kilograms in the market's own weight unit, items, money in the method's currency, or a named attribute.
| Route | Semantics |
|---|---|
POST …/tiers | Adds one row, leaving the rest alone — the edit for a merchant adding a heavier bracket. |
PUT …/tiers | Lays a whole table down at once. Set semantics. |
POST …/tiers/ladder | Generates an evenly stepped table. |
GET /v1/shipping/methods/{method_id}/tiers | The whole table. |
GET /v1/shipping/methods/{method_id}/tiers/{id} | One row. |
PUT /v1/shipping/methods/{method_id}/tiers/{id} | One row, edited in place. |
DELETE /v1/shipping/methods/{method_id}/tiers/{id} | One row removed. |
PUT …/tiers and the ladder route discard the ids of the rows they replace. Anything holding a tier id has to re-read.Two rows may not share method_id + from_value — that is the 409. method_id is taken from the path on every write, so a body naming a different method is ignored rather than obeyed.
Read, edit and remove one tier
curl "https://api.revenexx.com/v1/shipping/methods/$METHOD_ID/tiers?from_value=10" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
GET /v1/shipping/methods/{method_id}/tiers lists the table in no guaranteed order; send ?order=from_value.asc for lowest first. The API reference still says the list comes lowest threshold first; the API gives no guaranteed order, as described here. ?from_value= addresses the one row that prices that threshold. It pages with limit (default 50, at most 200) and offset, sorts with order, and answers items, page and filter. A query key that is neither a column nor one of those is 400 unknown_filter.
400:method_idis not a uuid, orordernames a column the tier does not have.404: no method with thatmethod_idin this tenant. It is never an empty page.
GET /v1/shipping/methods/{method_id}/tiers/{id} reads one row. A tier id that belongs to another method is 404. A tier id is not durable: PUT …/tiers and the ladder replace the rows, so an id read before either of them names nothing afterwards. For a stable handle, read by threshold with ?from_value=.
PUT /v1/shipping/methods/{method_id}/tiers/{id} is a partial update of from_value, price or position. Its status codes:
400: the id is not a uuid, the body names no column to write, a negativefrom_valueorprice(check_violation), or a changed platform-written field such ascreated_at(server_owned_field).404: no tier with that id under this method.409(unique_violation): this method already has a tier at thatfrom_value.
curl -X PUT "https://api.revenexx.com/v1/shipping/methods/$METHOD_ID/tiers/$TIER_ID" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"price":9.50}'
DELETE /v1/shipping/methods/{method_id}/tiers/{id} removes one row and answers {"deleted": true, "id": "…"}. What it does to the price depends on which row goes:
- A tier in the middle is harmless: its measures fall to the highest remaining threshold below them.
- The lowest tier is not: a measure under the new lowest threshold matches no tier, so the method leaves
POST /v1/shipping/rateswithno tier covers measure …. - Deleting the last remaining tier leaves the table empty and takes the method out of the checkout altogether, excluded as
matrix has no rate tiers configured.
Neither case applies to a basket that is already free (free-above) or over quote_above: those are checked before the tiers, so such a basket is still offered.
To rebuild the whole table, use PUT …/tiers or the ladder. Deleting the method deletes its tiers too.
The ladder generator
curl -X POST "https://api.revenexx.com/v1/shipping/methods/$METHOD_ID/tiers/ladder" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"from_value":0,"to_value":30,"step":5,"base_price":4.90,"step_price":2}'
The table a merchant describes in words — "0 to 30 kg, every 5 kg, €4.90 plus €2 a step" — without typing every row. It replaces the method's tiers by default; send replace: false to append.
Resolving rates
curl -X POST https://api.revenexx.com/v1/shipping/rates \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "X-Revenexx-Market: de" \
-H "Content-Type: application/json" \
-d '{
"country": "DE",
"order_value": 129.90,
"weight": 7.4,
"currency": "EUR"
}'
The question a checkout asks, and the only route that answers a price. Hand in the destination country, the order value, and whatever the matrix methods measure — a weight, a quantity or a named product attribute.
Back comes:
| Key | Is |
|---|---|
rates | The methods that apply, in position order — which is the order the checkout should print, not cheapest-first unless the merchant ordered them so. Each with its price, free-above threshold, the carrier's delivery promise and tax already applied. Empty is a legitimate answer. |
excluded | The enabled methods that did not apply, each with the sentence saying why. This is the diagnostic half of the answer, and it is why an empty rates never needs a support ticket to explain. |
basis | How the answer was measured — the tenant settings that shaped it, echoed so the numbers can be re-derived. |
An excluded reason is in words: disabled, a country the method or its carrier does not serve, a carrier that is paused or retired, a matrix missing its measure in the request, a matrix with no tiers configured, or no tier covering the measure.
tax.resolved: false means the rates are UNKNOWN, not untaxed. A tax rate that cannot be resolved is not an error, and it is not zero.X-Revenexx-Market genuinely restricts the candidates here — methods assigned to this market plus the unassigned ones — which is one of only two routes where the header affects a read.
Seeding a shop
curl -X POST https://api.revenexx.com/v1/shipping/methods/defaults \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{}'
Runs the carrier seed first, then creates any missing method: the three lines a shop is expected to offer — standard, express and pickup. The app runs it on install; calling it by hand afterwards is how a tenant that deleted one gets it back, and calling it twice costs nothing because it reconciles rather than seeds.
The seeded methods deliberately name no carrier — which carrier carries the standard method is a contract, not a default.
The tax-class usage check
curl "https://api.revenexx.com/v1/shipping/tax-classes/reduced/usage" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
Markets is the source of record for a tax rate, and this app points at it by code from two places: a method's own tax_class, and the tenant's shipping_tax_class fallback. Neither is a foreign key, and neither could be — a cross-app foreign key is not something the platform allows. So integrity is a question one app asks the other, and this route is the answering half.
It is asked before a destructive edit: the markets app calls it when an operator tries to delete a tax class, and a count above zero is what stops the delete rather than leaving these methods pointing at a code nobody serves.
Matched as a code, not a row: a tax class is unique per market, so reduced may exist in several and a method naming it does not say which one it meant. Send X-Revenexx-Market to count only that market's own methods plus the unassigned ones; without it every method of the tenant counts. The answer carries count and samples, the field names every app's usage answer shares. count is the number of matching methods, capped at 500: 500 means 500 or more. samples names 20 of them, in no set order. shipping_methods and methods carry the same two values under their older names, kept for existing callers: shipping_methods equals count, and methods is the same list as samples. Every code answers — one nobody points at is in_use: false, never a 404.
That pattern is worth copying in your own app. When you point at another app's row by code, publish the usage route that lets it ask.
Where to go next
- Carriers and tracking — who carries it, and the tracking link.
- Markets — where tax classes and their rates live.
- Orders — where a chosen method and its carrier land on a shipment.