Shipping Provider API
Overview
Shipping zones no longer own methods. A zone owns provider attachments, and each provider decides what methods it offers inside that zone. EverShop ships one provider, core, whose methods and per-zone rates are admin-managed through the endpoints on this page.
The full configuration path is:
- Create a zone — the geography. See the Shipping Zone API.
- Attach a provider to the zone. Creating a zone attaches
coreautomatically. - For
core, create methods (zone-independent options such as "Standard" or "Express"). - Give each method a per-zone rate, which is what determines the price in that zone.
The following routes were removed and now return 404:
POST /api/shippingZones/{id}/methodsPATCH /api/shippingZones/{zone_id}/methods/{method_id}DELETE /api/shippingZones/{zone_id}/methods/{method_id}POST /api/shippingMethodsPATCH /api/shippingMethods/{id}
Migrate to /api/shippingZones/{zone_id}/providers[...] for attachments and /api/shippingProviders/core/methods plus /api/shippingProviders/core/rates for the Core provider's configuration.
Two payload fields disappeared with them. calculation_type (flat_rate / price_based_rate / weight_based_rate / api) no longer exists at all — the shape of the cost is inferred from which cost field you populate on the rate. And condition_type now accepts only "price", "weight" or null; the old "none" value is rejected by the schema.
There is no shipping_provider table. Providers are registered with registerShippingProvider(...) from a module's bootstrap.ts, and shipping_zone_provider.provider_code is a soft reference into that registry. Codes must be unique across all installed extensions, and registration after bootstrap throws.
Zone Provider Attachments
Attach A Provider To A Zone
Makes a registered provider offer its methods inside the zone. {zone_id} is the zone uuid.
| Field Name | Field Type | Required |
|---|---|---|
| provider_code | string | Yes |
| config | object | No |
| is_enabled | boolean | No |
| sort_order | integer | No |
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/shippingZones/a1b2c3d4-e5f6-7890-abcd-ef1234567890/providers
fetch('https://<your domain>/api/shippingZones/a1b2c3d4-e5f6-7890-abcd-ef1234567890/providers', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
},
body: <JSON DATA>
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {
"shipping_zone_provider_id": 8,
"uuid": "b7c3f1a2-4d5e-4a6b-9c8d-0e1f2a3b4c5d",
"zone_id": 3,
"provider_code": "core",
"is_enabled": true,
"sort_order": 0,
"config": {}
}
}
config holds per-zone provider configuration, whose shape the provider declares through zoneConfigFields — a per-zone markup percentage, for example. The endpoint accepts any object; the admin UI is what validates it against the declared fields. The core provider declares no zone config fields, so its attachments carry {}.
Defaults: config {}, is_enabled true, sort_order 0. additionalProperties is false, so an unrecognised key is rejected.
Errors: 400 Invalid zone id, 400 Shipping provider "x" is not registered, 400 Provider "x" is already attached to this zone.
Update A Zone Provider Attachment
Patches the attachment identified by (zone uuid, provider_code). Only the fields present in the body are written.
| Field Name | Field Type | Required |
|---|---|---|
| is_enabled | boolean | No |
| config | object | No |
| sort_order | integer | No |
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/shippingZones/a1b2c3d4-e5f6-7890-abcd-ef1234567890/providers/core
fetch('https://<your domain>/api/shippingZones/a1b2c3d4-e5f6-7890-abcd-ef1234567890/providers/core', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
},
body: <JSON DATA>
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {
"shipping_zone_provider_id": 8,
"uuid": "b7c3f1a2-4d5e-4a6b-9c8d-0e1f2a3b4c5d",
"zone_id": 3,
"provider_code": "core",
"is_enabled": false,
"sort_order": 0,
"config": {}
}
}
provider_code itself cannot be changed — detach and re-attach instead.
Errors: 400 Invalid zone id, 400 Provider is not attached to this zone.
Detach A Provider From A Zone
Removes the attachment row. Takes no request body.
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
https://<your domain>/api/shippingZones/a1b2c3d4-e5f6-7890-abcd-ef1234567890/providers/core
fetch('https://<your domain>/api/shippingZones/a1b2c3d4-e5f6-7890-abcd-ef1234567890/providers/core', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
}
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {
"detached": true
}
}
Detaching is non-destructive to provider-internal data. Core's core_shipping_method_rate rows are keyed on the zone, not on the attachment, so re-attaching later re-exposes the existing rates without re-entering them.
There is no registry validation on this route — an attachment whose provider is no longer installed can still be cleaned up. Deleting a zone that does not exist returns 400 Invalid zone id; detaching a provider that was never attached succeeds silently.
Core Provider Methods
A Core method is the logical, zone-independent shipping option. It carries no price — the price lives on a rate.
Create A Core Method
| Field Name | Field Type | Required |
|---|---|---|
| name | string | Yes |
| is_enabled | boolean | No |
| sort_order | integer | No |
| default_carrier_code | string or null | No |
| default_service_code | string or null | No |
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/shippingProviders/core/methods
fetch('https://<your domain>/api/shippingProviders/core/methods', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
},
body: <JSON DATA>
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {
"core_shipping_method_id": 4,
"uuid": "0f1c8b7a-9d3e-4f2a-a1b6-77d4c2e9b510",
"name": "Standard Shipping",
"is_enabled": true,
"sort_order": 10,
"default_carrier_code": null,
"default_service_code": null
}
}
Method names are unique. Defaults: is_enabled true, sort_order 0, both default codes null.
The returned uuid is the value that surfaces as AvailableShippingMethod.code at the storefront and that you send back as method_code when selecting a method on a cart.
The two default_* fields are fulfillment hints
Neither is ever shown to a customer, and neither is validated by core.
| Field | What it does |
|---|---|
default_carrier_code | Written into shipping_method_data.snapshot.carrier at checkout. The admin ship dialog uses it to pre-select the carrier dropdown. |
default_service_code | Written into shipping_method_data.snapshot.serviceCode at checkout, then threaded into the carrier's label request so the label prints for the service the customer actually paid for. Free-form — service codes are carrier-specific vocabulary such as FEDEX_GROUND. |
Errors: 400 A Core method with name "x" already exists.
Update A Core Method
{id} is the method uuid. Only the fields present in the body are written.
| Field Name | Field Type | Required |
|---|---|---|
| name | string | No |
| is_enabled | boolean | No |
| sort_order | integer | No |
| default_carrier_code | string or null | No |
| default_service_code | string or null | No |
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/shippingProviders/core/methods/0f1c8b7a-9d3e-4f2a-a1b6-77d4c2e9b510
fetch('https://<your domain>/api/shippingProviders/core/methods/0f1c8b7a-9d3e-4f2a-a1b6-77d4c2e9b510', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
},
body: <JSON DATA>
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {
"core_shipping_method_id": 4,
"uuid": "0f1c8b7a-9d3e-4f2a-a1b6-77d4c2e9b510",
"name": "Standard Shipping",
"is_enabled": false,
"sort_order": 10,
"default_carrier_code": "custom",
"default_service_code": null
}
}
Errors: 400 Core method not found, 400 A Core method with name "x" already exists.
Delete A Core Method
{id} is the method uuid. Takes no request body. Deleting a method cascades to all of its core_shipping_method_rate rows.
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
https://<your domain>/api/shippingProviders/core/methods/0f1c8b7a-9d3e-4f2a-a1b6-77d4c2e9b510
fetch('https://<your domain>/api/shippingProviders/core/methods/0f1c8b7a-9d3e-4f2a-a1b6-77d4c2e9b510', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
}
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {
"core_shipping_method_id": 4,
"uuid": "0f1c8b7a-9d3e-4f2a-a1b6-77d4c2e9b510",
"name": "Standard Shipping",
"is_enabled": true,
"sort_order": 10,
"default_carrier_code": null,
"default_service_code": null
}
}
Carts that had already selected the deleted method surface a "method no longer available" error on the next recompute and have the selection cleared, so the customer re-picks at checkout.
Errors: 400 Core method not found.
Core Provider Rates
A rate binds one Core method to one zone and says what it costs there. Exactly one rate may exist per (method, zone) pair.
How the cost is chosen
There is no calculation_type field. The provider inspects the rate's cost fields in a fixed order and uses the first one that is populated:
| Priority | Field | Result |
|---|---|---|
| 1 | cost | Flat rate. Any non-null value wins outright, so null the other two when you want tiers. |
| 2 | price_based_cost | Tiered by cart value. Array of { min_price, cost }. |
| 3 | weight_based_cost | Tiered by cart weight. Array of { min_weight, cost }. |
| — | none populated | The rate is silently skipped at quote time. Treat this as a misconfiguration. |
Tiers are sorted by their minimum and the highest tier whose minimum is less than or equal to the cart value wins, so each tier's range is closed at the bottom and ended by the next tier's minimum. A cart below the lowest tier costs 0.
How the condition gates the rate
condition_type decides whether the rate applies at all, independently of how the cost is computed.
| Value | Behaviour |
|---|---|
null | The rate always applies. min and max are ignored. |
"price" | The cart's value must fall in the half-open interval [min, max). |
"weight" | The cart's weight must fall in the half-open interval [min, max). |
The interval is half-open on purpose: a cart sitting exactly on max is excluded, so adjacent rates no longer both match at the boundary. A null bound is simply unbounded on that side. The old "none" value is not accepted — use null.
Create A Rate
| Field Name | Field Type | Required |
|---|---|---|
| method_id | string | Yes |
| zone_id | string | Yes |
| is_enabled | boolean | No |
| cost | string or number or null | No |
| condition_type | string or null ('price', 'weight', ) | No |
| min | string or number or null | No |
| max | string or number or null | No |
| price_based_cost | array or null | No |
| weight_based_cost | array or null | No |
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/shippingProviders/core/rates
fetch('https://<your domain>/api/shippingProviders/core/rates', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
},
body: <JSON DATA>
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {
"core_shipping_method_rate_id": 11,
"uuid": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
"method_id": 4,
"zone_id": 3,
"is_enabled": true,
"cost": "5.99",
"condition_type": null,
"min": null,
"max": null,
"price_based_cost": null,
"weight_based_cost": null
}
}
method_id and zone_id are uuids in the request body; the response echoes the resolved numeric ids. Everything else defaults to null, except is_enabled, which defaults to true.
A free-shipping-over-100 rate, tiered by cart value:
{
"method_id": "0f1c8b7a-9d3e-4f2a-a1b6-77d4c2e9b510",
"zone_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"is_enabled": true,
"cost": null,
"condition_type": null,
"min": null,
"max": null,
"price_based_cost": [
{ "min_price": 0, "cost": 9.95 },
{ "min_price": 100, "cost": 0 }
],
"weight_based_cost": null
}
An express rate restricted to carts under 20 weight units:
{
"method_id": "6b3d9e1f-2a4c-4d7e-8b90-1c2d3e4f5a6b",
"zone_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"cost": 24.5,
"condition_type": "weight",
"min": 0,
"max": 20,
"price_based_cost": null,
"weight_based_cost": null
}
Errors: 400 Core method not found, 400 Invalid zone id, 400 A rate for this method and zone already exists. Edit it instead.
Update A Rate
{uuid} is the rate uuid. This is a full state replacement rather than a merge — every field you omit is written as null (is_enabled falls back to true). Send the complete rate state, including explicit nulls for the cost branches you are not using.
| Field Name | Field Type | Required |
|---|---|---|
| is_enabled | boolean | No |
| cost | string or number or null | No |
| condition_type | string or null ('price', 'weight', ) | No |
| min | string or number or null | No |
| max | string or number or null | No |
| price_based_cost | array or null | No |
| weight_based_cost | array or null | No |
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/shippingProviders/core/rates/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f
fetch('https://<your domain>/api/shippingProviders/core/rates/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
},
body: <JSON DATA>
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {
"core_shipping_method_rate_id": 11,
"uuid": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
"method_id": 4,
"zone_id": 3,
"is_enabled": true,
"cost": "7.99",
"condition_type": null,
"min": null,
"max": null,
"price_based_cost": null,
"weight_based_cost": null
}
}
The method and zone bindings are fixed at creation and cannot be patched. To move a rate, delete it and create a new one.
Errors: 400 Core shipping rate not found.
Delete A Rate
{uuid} is the rate uuid. Takes no request body. The method itself survives; it simply stops being offered in that zone until a new rate is created.
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
https://<your domain>/api/shippingProviders/core/rates/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f
fetch('https://<your domain>/api/shippingProviders/core/rates/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
}
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {
"deleted": true
}
}
Errors: 400 Core shipping rate not found.
Related Documentation
- Shipping Provider Development — writing your own provider.
- Shipping Zone API — creating and editing the geography.
- Shipping Method API — selecting a method on a cart at checkout.
- Shipment API — fulfilling the order once it is placed.