Configure shipping zones, rates, methods, providers, and per-zip overrides
/admin/api/shipping/zones
List all shipping zones.
/admin/api/shipping/zones
Create a new shipping zone.
/admin/api/shipping/zones/{id}
Update a shipping zone's name and country list.
/admin/api/shipping/zones/{id}
Delete a shipping zone and its associated rates.
/admin/api/shipping/zones/{id}/rates
List all shipping rates for a specific zone.
/admin/api/shipping/zones/{id}/rates
Set (upsert) shipping rates for a zone. Each (zone, method) pair is created or updated.
/admin/api/shipping/methods
List all shipping methods.
/admin/api/shipping/methods
Create a new shipping method.
/admin/api/shipping/methods/{id}
Update a shipping method.
/admin/api/shipping/methods/{id}
Delete a shipping method and its associated rates.
/admin/api/shipping/settings
Get the free-shipping threshold and the minimum order value. A value of 0 means the corresponding feature is disabled.
/admin/api/shipping/settings
Update the free-shipping threshold and/or the minimum order value. Both fields are optional — only the fields supplied are changed, so each settings page can mutate just its own value. Pass 0 to disable a feature.
/admin/api/shipping/providers
List every built-in shipping provider with its current enabled flag. Providers without a row in the integrations table are returned with `enabled: false`.
/admin/api/shipping/providers/{id}
Toggle the `enabled` flag for one shipping provider. Existing `sandbox` and `config` values on the underlying integrations row are preserved — only `enabled` changes. The `{id}` segment must be one of the registered provider IDs (`flat_rate`, `easyship`, `free_shipping_threshold`).
/admin/api/shipping/zones/{zone_id}/zip-rates
List all zip-code rate overrides for a zone, with the method name resolved for convenience.
/admin/api/shipping/zones/{zone_id}/zip-rates
Replace every zip-code rate override on the zone with the supplied entries. Existing overrides not in the request are removed.
/admin/api/shipping/zones/{zone_id}/zip-rates/{id}
Delete a single zip-code rate override by primary key, scoped to the supplied zone. Cross-zone deletions return 404 even if the id exists in a different zone.
/admin/api/shipping-methods
List the shipping methods available for a destination country, with the resolved cost for each. Used by the admin create-order form's shipping-method picker. Only methods that have a configured rate for the country are returned.
/admin/api/shipping/boxnow/settings
Return the saved BoxNow integration config object used to pre-populate the settings form. Returns an empty object `{}` when no config has been saved yet.
/admin/api/shipping/boxnow/settings
Persist the full BoxNow integration config JSON. The body must be the complete config object the settings form holds in state — it replaces the stored `config`. This endpoint only writes config; it does not toggle the provider's `enabled` flag (use PUT /admin/api/shipping/providers/boxnow for that).
/admin/api/shipping/boxnow/validate-credentials
Probe BoxNow OAuth with the supplied transient credentials and, on success, fetch the account's warehouse origins in the same round-trip. Lets the settings form verify credentials and populate the warehouse picker before anything is saved to the database.
/admin/api/shipping/boxnow/origins
List the BoxNow warehouse origins for the settings-page dropdown, using the already-saved credentials. Used on page-load to refresh the warehouse picker of an already-configured integration.
/admin/api/shipments/{order_id}/boxnow/create
(Re-)create the BoxNow delivery request for an order. Reads the locker blob stored at checkout and re-issues the delivery request — useful to retry an order whose shipment is stuck in `exception` state. The body is ignored.
/admin/api/shipments/{order_id}/boxnow/label.pdf
Download the printable BoxNow PDF label for an order's shipment. ZeroShop fetches the label from the BoxNow Partner API server-side and streams back the raw PDF bytes (`application/pdf`) with an attachment Content-Disposition, so the OAuth token never reaches the browser. The response body is binary, not JSON.
/admin/api/shipments/{order_id}/econt/sync
Sync an Econt shipment after the merchant confirms the waybill in Econt's create_label.php iframe. Calls Econt's getTrace, then writes the waybill number, label PDF URL, and normalised status to the shipments row — creating the row if the original place-order callback failed. The body is ignored.
/admin/api/shipments/{order_id}/events
List the tracking-event log for an order's shipment, newest first. Events are recorded from provider webhooks. Returns an empty array when the shipment exists but has no events yet; returns 404 only when the order itself does not exist.
/admin/api/shipping/providers/speedy/services
Proxy Speedy's /services endpoint using the tenant's stored credentials and return the available shipping services. Works regardless of whether the Speedy provider is enabled, so admins can verify services before turning it on.
/admin/api/shipments/{order_id}/speedy/generate
Generate a Speedy waybill for an order from the destination data captured at checkout. Persists the waybill ID before finalizing so a finalize failure leaves a recoverable record. In production the waybill is finalized (status `label_purchased`); in sandbox mode finalize is skipped (status `created`). All body fields are optional.
/admin/api/shipments/{order_id}/speedy/cancel
Cancel a previously generated Speedy waybill for an order via Speedy's /shipment/cancel. On success the shipments row is set to `cancelled`. Speedy may reject the cancellation if the parcel has already been picked up. The body is optional.
/admin/api/shipments/speedy/pickup/ready
List every Speedy shipment that has a generated waybill but no pickup scheduled yet — the rows the merchant can bulk-select for a courier visit. Sorted newest-first.
/admin/api/shipments/speedy/pickup/scheduled
List every Speedy shipment that already has a courier pickup booked (its provider_meta carries a pickup_id), sorted by pickup date newest-first. A pickup_id of 0 means the booking was soft-confirmed.
/admin/api/shipments/speedy/pickup
Book a single Speedy courier visit that collects every selected shipment. Each shipment must be a Speedy shipment in `created` or `label_purchased` status with a generated waybill. On success each shipments row's provider_meta is stamped with the pickup info so it drops off the ready list.
/admin/api/shipping/zones
List all shipping zones.
{
"zones": [
{
"id": 1,
"name": "Domestic",
"countries": [
"US"
],
"created_at": "2025-11-20T14:30:00Z"
},
{
"id": 2,
"name": "EU",
"countries": [
"DE",
"FR",
"NL"
],
"created_at": "2025-11-19T09:15:00Z"
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/shipping/zones" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/shipping/zones",
headers={"Authorization": "Bearer zspat_..."}
)
zones = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/zones", {
headers: { "Authorization": "Bearer zspat_..." }
});
const data = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/zones")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/shipping/zones
Create a new shipping zone.
{
"name": "EU",
"countries": [
"DE",
"FR",
"NL"
]
}
{
"id": 2,
"name": "EU",
"countries": [
"DE",
"FR",
"NL"
],
"created_at": "2025-11-20T14:30:00Z"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/shipping/zones" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{
"name": "EU",
"countries": ["DE", "FR", "NL"]
}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/shipping/zones",
headers={"Authorization": "Bearer zspat_..."},
json={"name": "EU", "countries": ["DE", "FR", "NL"]}
)
zone = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/zones", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ name: "EU", countries: ["DE", "FR", "NL"] })
});
const zone = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/zones")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { name: "EU", countries: %w[DE FR NL] }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
zone = JSON.parse(resp.body)
/admin/api/shipping/zones/{id}
Update a shipping zone's name and country list.
{
"name": "European Union",
"countries": [
"DE",
"FR",
"NL",
"BE"
]
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/shipping/zones/2" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{
"name": "European Union",
"countries": ["DE", "FR", "NL", "BE"]
}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/shipping/zones/2",
headers={"Authorization": "Bearer zspat_..."},
json={"name": "European Union", "countries": ["DE", "FR", "NL", "BE"]}
)
assert resp.status_code == 204
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/zones/2", {
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ name: "European Union", countries: ["DE", "FR", "NL", "BE"] })
});
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/zones/2")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { name: "European Union", countries: %w[DE FR NL BE] }.to_json
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/shipping/zones/{id}
Delete a shipping zone and its associated rates.
curl -X DELETE "https://yourshop.zeroshop.io/admin/api/shipping/zones/2" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.delete(
"https://yourshop.zeroshop.io/admin/api/shipping/zones/2",
headers={"Authorization": "Bearer zspat_..."}
)
assert resp.status_code == 204
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/zones/2", {
method: "DELETE",
headers: { "Authorization": "Bearer zspat_..." }
});
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/zones/2")
req = Net::HTTP::Delete.new(uri)
req["Authorization"] = "Bearer zspat_..."
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/shipping/zones/{id}/rates
List all shipping rates for a specific zone.
{
"rates": [
{
"id": 1,
"zone_id": 1,
"method_id": 1,
"rate": 599,
"created_at": "2025-11-20T14:30:00Z"
},
{
"id": 2,
"zone_id": 1,
"method_id": 2,
"rate": 999,
"created_at": "2025-11-20T14:30:00Z"
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/shipping/zones/1/rates" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/shipping/zones/1/rates",
headers={"Authorization": "Bearer zspat_..."}
)
rates = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/zones/1/rates", {
headers: { "Authorization": "Bearer zspat_..." }
});
const data = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/zones/1/rates")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/shipping/zones/{id}/rates
Set (upsert) shipping rates for a zone. Each (zone, method) pair is created or updated.
{
"rates": [
{
"method_id": 1,
"rate": 599
},
{
"method_id": 2,
"rate": 999
}
]
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/shipping/zones/1/rates" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{
"rates": [
{"method_id": 1, "rate": 599},
{"method_id": 2, "rate": 999}
]
}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/shipping/zones/1/rates",
headers={"Authorization": "Bearer zspat_..."},
json={"rates": [{"method_id": 1, "rate": 599}, {"method_id": 2, "rate": 999}]}
)
assert resp.status_code == 204
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/zones/1/rates", {
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
rates: [
{ method_id: 1, rate: 599 },
{ method_id: 2, rate: 999 }
]
})
});
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/zones/1/rates")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { rates: [{ method_id: 1, rate: 599 }, { method_id: 2, rate: 999 }] }.to_json
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/shipping/methods
List all shipping methods.
{
"methods": [
{
"id": 1,
"name": "Standard Shipping",
"description": "5-7 business days",
"enabled": true,
"sort_order": 0,
"created_at": "2025-11-20T14:30:00Z"
},
{
"id": 2,
"name": "Express Shipping",
"description": "1-2 business days",
"enabled": true,
"sort_order": 1,
"created_at": "2025-11-19T09:15:00Z"
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/shipping/methods" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/shipping/methods",
headers={"Authorization": "Bearer zspat_..."}
)
methods = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/methods", {
headers: { "Authorization": "Bearer zspat_..." }
});
const data = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/methods")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/shipping/methods
Create a new shipping method.
{
"name": "Express Shipping",
"description": "1-2 business days"
}
{
"id": 2,
"name": "Express Shipping",
"description": "1-2 business days",
"enabled": true,
"sort_order": 0,
"created_at": "2025-11-20T14:30:00Z"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/shipping/methods" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Express Shipping",
"description": "1-2 business days"
}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/shipping/methods",
headers={"Authorization": "Bearer zspat_..."},
json={"name": "Express Shipping", "description": "1-2 business days"}
)
method = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/methods", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ name: "Express Shipping", description: "1-2 business days" })
});
const method = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/methods")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { name: "Express Shipping", description: "1-2 business days" }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
method = JSON.parse(resp.body)
/admin/api/shipping/methods/{id}
Update a shipping method.
{
"name": "Express Shipping",
"description": "1-2 business days",
"enabled": true,
"sort_order": 1
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/shipping/methods/2" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Express Shipping",
"description": "1-2 business days",
"enabled": true,
"sort_order": 1
}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/shipping/methods/2",
headers={"Authorization": "Bearer zspat_..."},
json={
"name": "Express Shipping",
"description": "1-2 business days",
"enabled": True,
"sort_order": 1
}
)
assert resp.status_code == 204
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/methods/2", {
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "Express Shipping",
description: "1-2 business days",
enabled: true,
sort_order: 1
})
});
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/methods/2")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { name: "Express Shipping", description: "1-2 business days", enabled: true, sort_order: 1 }.to_json
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/shipping/methods/{id}
Delete a shipping method and its associated rates.
curl -X DELETE "https://yourshop.zeroshop.io/admin/api/shipping/methods/2" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.delete(
"https://yourshop.zeroshop.io/admin/api/shipping/methods/2",
headers={"Authorization": "Bearer zspat_..."}
)
assert resp.status_code == 204
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/methods/2", {
method: "DELETE",
headers: { "Authorization": "Bearer zspat_..." }
});
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/methods/2")
req = Net::HTTP::Delete.new(uri)
req["Authorization"] = "Bearer zspat_..."
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/shipping/settings
Get the free-shipping threshold and the minimum order value. A value of 0 means the corresponding feature is disabled.
{
"free_shipping_threshold": 5000,
"min_order_value": 2500
}
curl "https://yourshop.zeroshop.io/admin/api/shipping/settings" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/shipping/settings",
headers={"Authorization": "Bearer zspat_..."}
)
settings = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/settings", {
headers: { "Authorization": "Bearer zspat_..." }
});
const settings = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/settings")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
settings = JSON.parse(resp.body)
/admin/api/shipping/settings
Update the free-shipping threshold and/or the minimum order value. Both fields are optional — only the fields supplied are changed, so each settings page can mutate just its own value. Pass 0 to disable a feature.
{
"free_shipping_threshold": 5000,
"min_order_value": 2500
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/shipping/settings" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"free_shipping_threshold": 5000, "min_order_value": 2500}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/shipping/settings",
headers={"Authorization": "Bearer zspat_..."},
json={"free_shipping_threshold": 5000, "min_order_value": 2500}
)
assert resp.status_code == 204
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/settings", {
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ free_shipping_threshold: 5000, min_order_value: 2500 })
});
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/settings")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { free_shipping_threshold: 5000, min_order_value: 2500 }.to_json
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/shipping/providers
List every built-in shipping provider with its current enabled flag. Providers without a row in the integrations table are returned with `enabled: false`.
[
{
"id": "flat_rate",
"name": "Flat rate",
"description": "Charge a fixed shipping cost based on the customer's country.",
"enabled": true
},
{
"id": "easyship",
"name": "Easyship",
"description": "Buy real shipping labels and track parcels via the Easyship API.",
"enabled": false
},
{
"id": "free_shipping_threshold",
"name": "Free shipping threshold",
"description": "Waive shipping costs when the cart subtotal reaches a set amount.",
"enabled": true
}
]
curl "https://yourshop.zeroshop.io/admin/api/shipping/providers" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/shipping/providers",
headers={"Authorization": "Bearer zspat_..."}
)
providers = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/providers", {
headers: { "Authorization": "Bearer zspat_..." }
});
const providers = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/providers")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
providers = JSON.parse(resp.body)
/admin/api/shipping/providers/{id}
Toggle the `enabled` flag for one shipping provider. Existing `sandbox` and `config` values on the underlying integrations row are preserved — only `enabled` changes. The `{id}` segment must be one of the registered provider IDs (`flat_rate`, `easyship`, `free_shipping_threshold`).
{
"enabled": true
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/shipping/providers/easyship" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
import requests
requests.put(
"https://yourshop.zeroshop.io/admin/api/shipping/providers/easyship",
headers={"Authorization": "Bearer zspat_..."},
json={"enabled": True}
)
await fetch("https://yourshop.zeroshop.io/admin/api/shipping/providers/easyship", {
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ enabled: true })
});
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/providers/easyship")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { enabled: true }.to_json
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/shipping/zones/{zone_id}/zip-rates
List all zip-code rate overrides for a zone, with the method name resolved for convenience.
[
{
"id": 12,
"method_id": 1,
"method_name": "Standard Shipping",
"postal_code": "1000",
"rate": 399
},
{
"id": 13,
"method_id": 2,
"method_name": "Express Shipping",
"postal_code": "1000",
"rate": 799
}
]
curl "https://yourshop.zeroshop.io/admin/api/shipping/zones/2/zip-rates" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/shipping/zones/2/zip-rates",
headers={"Authorization": "Bearer zspat_..."}
)
overrides = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/zones/2/zip-rates", {
headers: { "Authorization": "Bearer zspat_..." }
});
const overrides = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/zones/2/zip-rates")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
overrides = JSON.parse(resp.body)
/admin/api/shipping/zones/{zone_id}/zip-rates
Replace every zip-code rate override on the zone with the supplied entries. Existing overrides not in the request are removed.
{
"entries": [
{
"postal_code": "1000",
"method_id": 1,
"rate": 399
},
{
"postal_code": "1000",
"method_id": 2,
"rate": 799
}
]
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/shipping/zones/2/zip-rates" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{
"entries": [
{"postal_code": "1000", "method_id": 1, "rate": 399},
{"postal_code": "1000", "method_id": 2, "rate": 799}
]
}'
import requests
requests.put(
"https://yourshop.zeroshop.io/admin/api/shipping/zones/2/zip-rates",
headers={"Authorization": "Bearer zspat_..."},
json={"entries": [
{"postal_code": "1000", "method_id": 1, "rate": 399},
{"postal_code": "1000", "method_id": 2, "rate": 799}
]}
)
await fetch("https://yourshop.zeroshop.io/admin/api/shipping/zones/2/zip-rates", {
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
entries: [
{ postal_code: "1000", method_id: 1, rate: 399 },
{ postal_code: "1000", method_id: 2, rate: 799 }
]
})
});
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/zones/2/zip-rates")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = {
entries: [
{ postal_code: "1000", method_id: 1, rate: 399 },
{ postal_code: "1000", method_id: 2, rate: 799 }
]
}.to_json
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/shipping/zones/{zone_id}/zip-rates/{id}
Delete a single zip-code rate override by primary key, scoped to the supplied zone. Cross-zone deletions return 404 even if the id exists in a different zone.
curl -X DELETE "https://yourshop.zeroshop.io/admin/api/shipping/zones/2/zip-rates/12" \
-H "Authorization: Bearer zspat_..."
import requests
requests.delete(
"https://yourshop.zeroshop.io/admin/api/shipping/zones/2/zip-rates/12",
headers={"Authorization": "Bearer zspat_..."}
)
await fetch("https://yourshop.zeroshop.io/admin/api/shipping/zones/2/zip-rates/12", {
method: "DELETE",
headers: { "Authorization": "Bearer zspat_..." }
});
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/zones/2/zip-rates/12")
req = Net::HTTP::Delete.new(uri)
req["Authorization"] = "Bearer zspat_..."
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/shipping-methods
List the shipping methods available for a destination country, with the resolved cost for each. Used by the admin create-order form's shipping-method picker. Only methods that have a configured rate for the country are returned.
| Name | Type | Required | Description |
|---|---|---|---|
| country | string | required | ISO 3166-1 alpha-2 destination country code (e.g. BG, DE) to resolve methods and rates for. |
{
"methods": [
{
"id": 1,
"name": "Standard Shipping",
"cost_cents": 599
},
{
"id": 2,
"name": "Express Shipping",
"cost_cents": 999
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/shipping-methods?country=BG" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/shipping-methods",
headers={"Authorization": "Bearer zspat_..."},
params={"country": "BG"}
)
methods = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping-methods?country=BG", {
headers: { "Authorization": "Bearer zspat_..." }
});
const data = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping-methods")
uri.query = URI.encode_www_form(country: "BG")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/shipping/boxnow/settings
Return the saved BoxNow integration config object used to pre-populate the settings form. Returns an empty object `{}` when no config has been saved yet.
{
"client_id": "bn_client_123",
"client_secret": "bn_secret_***",
"api_url": "https://api-stage.boxnow.bg/api/v1",
"partner_id": 42,
"default_origin_id": "w-1",
"default_origin_label": "Main Warehouse",
"allow_cod": false
}
curl "https://yourshop.zeroshop.io/admin/api/shipping/boxnow/settings" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/shipping/boxnow/settings",
headers={"Authorization": "Bearer zspat_..."}
)
settings = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/boxnow/settings", {
headers: { "Authorization": "Bearer zspat_..." }
});
const settings = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/boxnow/settings")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
settings = JSON.parse(resp.body)
/admin/api/shipping/boxnow/settings
Persist the full BoxNow integration config JSON. The body must be the complete config object the settings form holds in state — it replaces the stored `config`. This endpoint only writes config; it does not toggle the provider's `enabled` flag (use PUT /admin/api/shipping/providers/boxnow for that).
{
"client_id": "bn_client_123",
"client_secret": "bn_secret_456",
"api_url": "https://api-stage.boxnow.bg/api/v1",
"partner_id": 42,
"default_origin_id": "w-1",
"default_origin_label": "Main Warehouse",
"allow_cod": false
}
{
"ok": true
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/shipping/boxnow/settings" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{
"client_id": "bn_client_123",
"client_secret": "bn_secret_456",
"api_url": "https://api-stage.boxnow.bg/api/v1",
"partner_id": 42,
"default_origin_id": "w-1",
"default_origin_label": "Main Warehouse",
"allow_cod": false
}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/shipping/boxnow/settings",
headers={"Authorization": "Bearer zspat_..."},
json={
"client_id": "bn_client_123",
"client_secret": "bn_secret_456",
"api_url": "https://api-stage.boxnow.bg/api/v1",
"partner_id": 42,
"default_origin_id": "w-1",
"default_origin_label": "Main Warehouse",
"allow_cod": False
}
)
result = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/boxnow/settings", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
client_id: "bn_client_123",
client_secret: "bn_secret_456",
api_url: "https://api-stage.boxnow.bg/api/v1",
partner_id: 42,
default_origin_id: "w-1",
default_origin_label: "Main Warehouse",
allow_cod: false
})
});
const result = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/boxnow/settings")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = {
client_id: "bn_client_123",
client_secret: "bn_secret_456",
api_url: "https://api-stage.boxnow.bg/api/v1",
partner_id: 42,
default_origin_id: "w-1",
default_origin_label: "Main Warehouse",
allow_cod: false
}.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
result = JSON.parse(resp.body)
/admin/api/shipping/boxnow/validate-credentials
Probe BoxNow OAuth with the supplied transient credentials and, on success, fetch the account's warehouse origins in the same round-trip. Lets the settings form verify credentials and populate the warehouse picker before anything is saved to the database.
{
"client_id": "bn_client_123",
"client_secret": "bn_secret_456",
"api_url": "https://api-stage.boxnow.bg/api/v1"
}
{
"ok": true,
"origins": [
{
"id": "w-1",
"name": "Main Warehouse",
"address_line_1": "ul. Vitosha 1",
"postal_code": "1000",
"country": "BG"
}
]
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/shipping/boxnow/validate-credentials" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{
"client_id": "bn_client_123",
"client_secret": "bn_secret_456",
"api_url": "https://api-stage.boxnow.bg/api/v1"
}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/shipping/boxnow/validate-credentials",
headers={"Authorization": "Bearer zspat_..."},
json={
"client_id": "bn_client_123",
"client_secret": "bn_secret_456",
"api_url": "https://api-stage.boxnow.bg/api/v1"
}
)
result = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/boxnow/validate-credentials", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
client_id: "bn_client_123",
client_secret: "bn_secret_456",
api_url: "https://api-stage.boxnow.bg/api/v1"
})
});
const result = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/boxnow/validate-credentials")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = {
client_id: "bn_client_123",
client_secret: "bn_secret_456",
api_url: "https://api-stage.boxnow.bg/api/v1"
}.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
result = JSON.parse(resp.body)
/admin/api/shipping/boxnow/origins
List the BoxNow warehouse origins for the settings-page dropdown, using the already-saved credentials. Used on page-load to refresh the warehouse picker of an already-configured integration.
[
{
"id": "w-1",
"name": "Main Warehouse",
"address_line_1": "ul. Vitosha 1",
"postal_code": "1000",
"country": "BG"
}
]
curl "https://yourshop.zeroshop.io/admin/api/shipping/boxnow/origins" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/shipping/boxnow/origins",
headers={"Authorization": "Bearer zspat_..."}
)
origins = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/boxnow/origins", {
headers: { "Authorization": "Bearer zspat_..." }
});
const origins = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/boxnow/origins")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
origins = JSON.parse(resp.body)
/admin/api/shipments/{order_id}/boxnow/create
(Re-)create the BoxNow delivery request for an order. Reads the locker blob stored at checkout and re-issues the delivery request — useful to retry an order whose shipment is stuck in `exception` state. The body is ignored.
{
"ok": true
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/shipments/123/boxnow/create" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/shipments/123/boxnow/create",
headers={"Authorization": "Bearer zspat_..."}
)
result = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipments/123/boxnow/create", {
method: "POST",
headers: { "Authorization": "Bearer zspat_..." }
});
const result = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipments/123/boxnow/create")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
result = JSON.parse(resp.body)
/admin/api/shipments/{order_id}/boxnow/label.pdf
Download the printable BoxNow PDF label for an order's shipment. ZeroShop fetches the label from the BoxNow Partner API server-side and streams back the raw PDF bytes (`application/pdf`) with an attachment Content-Disposition, so the OAuth token never reaches the browser. The response body is binary, not JSON.
curl "https://yourshop.zeroshop.io/admin/api/shipments/123/boxnow/label.pdf" \
-H "Authorization: Bearer zspat_..." \
-o boxnow-label.pdf
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/shipments/123/boxnow/label.pdf",
headers={"Authorization": "Bearer zspat_..."}
)
with open("boxnow-label.pdf", "wb") as f:
f.write(resp.content)
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipments/123/boxnow/label.pdf", {
headers: { "Authorization": "Bearer zspat_..." }
});
const blob = await resp.blob();
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipments/123/boxnow/label.pdf")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
File.binwrite("boxnow-label.pdf", resp.body)
/admin/api/shipments/{order_id}/econt/sync
Sync an Econt shipment after the merchant confirms the waybill in Econt's create_label.php iframe. Calls Econt's getTrace, then writes the waybill number, label PDF URL, and normalised status to the shipments row — creating the row if the original place-order callback failed. The body is ignored.
{
"shipment_id": 55,
"tracking_number": "1051234567890",
"tracking_url": "https://www.econt.com/services/track-shipment/1051234567890",
"label_url": "https://delivery.econt.com/label/1051234567890.pdf",
"status": "label_purchased",
"provider_status": "label_generated"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/shipments/123/econt/sync" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/shipments/123/econt/sync",
headers={"Authorization": "Bearer zspat_..."}
)
result = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipments/123/econt/sync", {
method: "POST",
headers: { "Authorization": "Bearer zspat_..." }
});
const result = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipments/123/econt/sync")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
result = JSON.parse(resp.body)
/admin/api/shipments/{order_id}/events
List the tracking-event log for an order's shipment, newest first. Events are recorded from provider webhooks. Returns an empty array when the shipment exists but has no events yet; returns 404 only when the order itself does not exist.
[
{
"id": 91,
"shipment_id": 55,
"provider": "boxnow",
"event_type": "delivered",
"occurred_at": "2026-05-28T13:00:00Z",
"received_at": "2026-05-28T13:00:05Z",
"location_display": "APM Sofia",
"location_postal": "1000",
"raw_payload": "{\"parcelState\":\"delivered\"}"
},
{
"id": 90,
"shipment_id": 55,
"provider": "boxnow",
"event_type": "in-depot",
"occurred_at": "2026-05-28T09:00:00Z",
"received_at": "2026-05-28T09:00:04Z",
"location_display": "APM Sofia",
"location_postal": "1000",
"raw_payload": "{\"parcelState\":\"in-depot\"}"
}
]
curl "https://yourshop.zeroshop.io/admin/api/shipments/123/events" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/shipments/123/events",
headers={"Authorization": "Bearer zspat_..."}
)
events = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipments/123/events", {
headers: { "Authorization": "Bearer zspat_..." }
});
const events = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipments/123/events")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
events = JSON.parse(resp.body)
/admin/api/shipping/providers/speedy/services
Proxy Speedy's /services endpoint using the tenant's stored credentials and return the available shipping services. Works regardless of whether the Speedy provider is enabled, so admins can verify services before turning it on.
{
"services": [
{
"id": 505,
"name": "Стандарт 24 часа",
"nameEn": "Standard 24 hours",
"description": "Office-to-office and office-to-door standard delivery."
},
{
"id": 202,
"name": "Експресна доставка",
"nameEn": "Express delivery",
"description": null
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/shipping/providers/speedy/services" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/shipping/providers/speedy/services",
headers={"Authorization": "Bearer zspat_..."}
)
services = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipping/providers/speedy/services", {
headers: { "Authorization": "Bearer zspat_..." }
});
const data = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipping/providers/speedy/services")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/shipments/{order_id}/speedy/generate
Generate a Speedy waybill for an order from the destination data captured at checkout. Persists the waybill ID before finalizing so a finalize failure leaves a recoverable record. In production the waybill is finalized (status `label_purchased`); in sandbox mode finalize is skipped (status `created`). All body fields are optional.
{
"contents": "Order #123",
"recipient_phone": "+359881234567"
}
{
"shipment_id": 55,
"speedy_shipment_id": 1000123456789,
"tracking_number": "1000123456789",
"tracking_url": "https://www.speedy.bg/en/track-shipment?shipmentNumber=1000123456789",
"label_url": "https://api.speedy.bg/label/1000123456789.pdf",
"status": "label_purchased",
"sandbox": false
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/shipments/123/speedy/generate" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"contents": "Order #123"}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/shipments/123/speedy/generate",
headers={"Authorization": "Bearer zspat_..."},
json={"contents": "Order #123"}
)
result = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipments/123/speedy/generate", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ contents: "Order #123" })
});
const result = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipments/123/speedy/generate")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { contents: "Order #123" }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
result = JSON.parse(resp.body)
/admin/api/shipments/{order_id}/speedy/cancel
Cancel a previously generated Speedy waybill for an order via Speedy's /shipment/cancel. On success the shipments row is set to `cancelled`. Speedy may reject the cancellation if the parcel has already been picked up. The body is optional.
{
"reason": "Customer cancelled the order"
}
{
"shipment_id": 55,
"speedy_shipment_id": 1000123456789,
"status": "cancelled"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/shipments/123/speedy/cancel" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"reason": "Customer cancelled the order"}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/shipments/123/speedy/cancel",
headers={"Authorization": "Bearer zspat_..."},
json={"reason": "Customer cancelled the order"}
)
result = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipments/123/speedy/cancel", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ reason: "Customer cancelled the order" })
});
const result = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipments/123/speedy/cancel")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { reason: "Customer cancelled the order" }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
result = JSON.parse(resp.body)
/admin/api/shipments/speedy/pickup/ready
List every Speedy shipment that has a generated waybill but no pickup scheduled yet — the rows the merchant can bulk-select for a courier visit. Sorted newest-first.
{
"shipments": [
{
"shipment_id": 55,
"order_id": 123,
"provider_shipment_id": "1000123456789",
"tracking_number": "1000123456789",
"status": "label_purchased",
"customer_name": "Ivan Petrov",
"total_cents": 4599
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/shipments/speedy/pickup/ready" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/shipments/speedy/pickup/ready",
headers={"Authorization": "Bearer zspat_..."}
)
ready = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipments/speedy/pickup/ready", {
headers: { "Authorization": "Bearer zspat_..." }
});
const data = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipments/speedy/pickup/ready")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/shipments/speedy/pickup/scheduled
List every Speedy shipment that already has a courier pickup booked (its provider_meta carries a pickup_id), sorted by pickup date newest-first. A pickup_id of 0 means the booking was soft-confirmed.
{
"shipments": [
{
"shipment_id": 55,
"order_id": 123,
"provider_shipment_id": "1000123456789",
"tracking_number": "1000123456789",
"pickup_id": 778899,
"pickup_date": "2026-05-31",
"visit_end_time": "18:00",
"customer_name": "Ivan Petrov"
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/shipments/speedy/pickup/scheduled" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/shipments/speedy/pickup/scheduled",
headers={"Authorization": "Bearer zspat_..."}
)
scheduled = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipments/speedy/pickup/scheduled", {
headers: { "Authorization": "Bearer zspat_..." }
});
const data = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipments/speedy/pickup/scheduled")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/shipments/speedy/pickup
Book a single Speedy courier visit that collects every selected shipment. Each shipment must be a Speedy shipment in `created` or `label_purchased` status with a generated waybill. On success each shipments row's provider_meta is stamped with the pickup info so it drops off the ready list.
{
"shipment_ids": [
55,
56
],
"date": "2026-05-31",
"visit_end_time": "18:00",
"contact_name": "Ivan Petrov",
"phone_number": "+359881234567"
}
{
"pickup_id": 778899,
"shipment_count": 2,
"date": "2026-05-31"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/shipments/speedy/pickup" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{
"shipment_ids": [55, 56],
"date": "2026-05-31",
"visit_end_time": "18:00",
"contact_name": "Ivan Petrov",
"phone_number": "+359881234567"
}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/shipments/speedy/pickup",
headers={"Authorization": "Bearer zspat_..."},
json={
"shipment_ids": [55, 56],
"date": "2026-05-31",
"visit_end_time": "18:00",
"contact_name": "Ivan Petrov",
"phone_number": "+359881234567"
}
)
result = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/shipments/speedy/pickup", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
shipment_ids: [55, 56],
date: "2026-05-31",
visit_end_time: "18:00",
contact_name: "Ivan Petrov",
phone_number: "+359881234567"
})
});
const result = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/shipments/speedy/pickup")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = {
shipment_ids: [55, 56],
date: "2026-05-31",
visit_end_time: "18:00",
contact_name: "Ivan Petrov",
phone_number: "+359881234567"
}.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
result = JSON.parse(resp.body)
We value your privacy
We use cookies for essential site functionality and, with your consent, analytics to understand how our platform is used. No personal data is shared with third parties. See our Privacy Policy for details.