Express delivery
Four endpoints in the shipping group cover same-day and express services: a price quote, a Wolt Drive shipment promise, and two ZigZag Express 24 eligibility checks.
These are not a workflow
Each of the four is dispatched to the carrier service class of the license's own company, and each is implemented by exactly one carrier:
| Endpoint | Implemented by | Method it needs |
|---|---|---|
same_day_delivery_quote | Get Package | getSameDayDeliveryQuote() |
create_shipping_promise | Wolt Drive | createShipmentPromise() |
get_express_delivery_enabled | ZigZag | getExpressDeliveryenabled() |
get_express_delivery_status | ZigZag | getExpressDeliveryStatus() |
A license whose carrier service does not define the method for the endpoint you call gets HTTP 400 and message: "Not supported for this provider" — the controller checks with method_exists() before doing anything else, including before $request->validate(). So there is no "check availability → quote → promise → status" sequence to follow: the two ZigZag checks belong together, the Get Package quote and the Wolt Drive promise are each standalone and are for different carriers.
These four are GET
Unlike the rest of the v1 shipping group, which is POST with a JSON body, these take all their parameters — credentials included — as query string values. Send them in the query string — that is the supported shape. A JSON body is also read on these GET routes: sending every parameter, credentials included, as JSON with Content-Type: application/json and an empty query string works. A form-encoded body does not — that answers 422.
Authentication
All four sit behind the same hasLicense middleware as the rest of the group, so license_key and domain are required on every call, as query parameters. A license key that cannot be resolved returns HTTP 403 with the standard envelope and message: "Invalid License Key" before the handler runs. See Authentication.
| Field | Type | Required | Description |
|---|---|---|---|
license_key | string | yes | The carrier account's license key. |
domain | string | yes | The calling store's domain. Required by validation; the license's own stored domain is not currently compared against it. |
token | string | no | Sanctum personal access token. HasLicense calls PersonalAccessToken::findToken() on every request; it only changes the outcome for licenses whose company is marked premium — without a premium-active user those licenses are refused with 403. |
Failures arrive in a fixed order, always: 403 license rejection, then the 400 provider guard, then 422 parameter validation. Because the method_exists() guard runs before $request->validate(), a call on an unsupported carrier can never produce a 422 — on a Cargo license, a request carrying only credentials and no other parameters answers 400 with "Not supported for this provider". The consequence is practical: you cannot discover an endpoint's parameter contract by probing it from a license whose carrier does not implement it.
422 responses on this page are not the four-key v1 envelope. They are Laravel's validation shape, {"message": ..., "errors": {...}}, and their messages are localised — Hebrew on the Israeli instances. A missing license_key or domain is one of these 422s, raised by hasLicense; only a key that resolves to no license gives the 403 envelope with "Invalid License Key".
A premium company is refused with 403 and one of two exact messages: "Premium companies are not supported in this version of plugin" when no user could be resolved from the token, and "You must be a premium member to use this company" when a user was resolved but is not premium-active.
Pickup (collection) addresses are not handled uniformly across the four. Only same_day_delivery_quote uses a collection address at all, and it reads collect_street, collect_street_number and collect_city from the license's stored settings — not collect_company, which none of the four endpoints read. create_shipping_promise uses the license's Wolt venue_id in place of an address, and the two ZigZag endpoints have no pickup-address concept. Nor is the collection address always the license's: when the license has no stored collection settings, License::getCollectionSettings() returns nulls and the handler falls through to a caller-supplied shipment_settings[collect_*] on the request, which then wins.
GET /shipping/same_day_delivery_quote
Price a same-day delivery with Get Package, from the license's collection address to the address you supply. Nothing is booked.
Parameters
Query
| Field | Type | Required | Description |
|---|---|---|---|
license_key | string | yes | See Authentication. |
domain | string | yes | See Authentication. |
city | string | yes | Destination city. |
street | string | yes | Destination street. |
street_number | integer | yes | Destination street number. Validated as an integer here (the other three endpoints accept a string). |
name | string | yes | Recipient name. Required by validation. |
phone | string | yes | Recipient phone. Required by validation. |
date | string | yes | Requested date, Y-m-d. Required by validation. |
package_size | string | yes | One of 0, 1, 2, 3, ENVELOPE, SMALL, MEDIUM, LARGE. |
TIP
name, phone and date are enforced by the validator but are not forwarded to Get Package by the current implementation, which sends only the two addresses and the package size. Send real values anyway — the validation is what it is, and the payload may widen.
package_size is a real parameter, but package_id cannot be set by a caller: the handler forwards only validated input, so the carrier layer's $data['package_id'] ?? Str::random() always takes the random branch. Do not try to supply it.
How the quote is refused
Before quoting, the service fetches Get Package's same-day settlement list (looked up for תל אביב, cached for 24 hours) and checks the license's collection city against it. A collection city that is not on the list returns success: false with message: "City {city} is not in location list".
Example request
curl --location --get 'https://app.shipos.co.il/api/shipping/same_day_delivery_quote' \
--header 'Accept: application/json' \
--data-urlencode 'license_key={license_key}' \
--data-urlencode 'domain=your-store.example.com' \
--data-urlencode 'city=תל אביב' \
--data-urlencode 'street=הרצל' \
--data-urlencode 'street_number=10' \
--data-urlencode 'name=Dana Levi' \
--data-urlencode 'phone=0501234567' \
--data-urlencode 'date=2026-09-01' \
--data-urlencode 'package_size=SMALL'// Node.js 18+ / browsers — no dependencies
const query = new URLSearchParams({
license_key: process.env.SHIPOS_LICENSE_KEY,
domain: process.env.SHIPOS_DOMAIN,
city: 'תל אביב',
street: 'הרצל',
street_number: '10',
name: 'Dana Levi',
phone: '0501234567',
date: '2026-09-01',
package_size: 'SMALL',
})
const response = await fetch(
`https://app.shipos.co.il/api/shipping/same_day_delivery_quote?${query}`,
{ headers: { Accept: 'application/json' } },
)
const body = await response.json()
if (!body.success) {
throw new Error(`ShipOS v1: ${body.message}`)
}
console.log(body.data)<?php
use Illuminate\Support\Facades\Http;
$body = Http::acceptJson()
->get('https://app.shipos.co.il/api/shipping/same_day_delivery_quote', [
'license_key' => config('services.shipos.license_key'),
'domain' => config('services.shipos.domain'),
'city' => 'תל אביב',
'street' => 'הרצל',
'street_number' => 10,
'name' => 'Dana Levi',
'phone' => '0501234567',
'date' => '2026-09-01',
'package_size' => 'SMALL',
])
->json();
if (! ($body['success'] ?? false)) {
throw new RuntimeException('ShipOS v1: '.$body['message']);
}Example response
data is Get Package's own quote body, passed through unchanged — this reference does not restate the carrier's schema.
{
"data": { "...": "..." },
"message": "",
"success": true,
"warning": ""
}A collection city outside the same-day list:
{
"data": [],
"message": "City חוות זוהר המדבר is not in location list",
"success": false,
"warning": ""
}The city named in that message is the license's collection city, not the destination city the request asked for — the example above sends city=תל אביב and is refused over a completely different place name.
Errors
| Status | When |
|---|---|
| 400 | The license's carrier does not implement same-day quoting (Not supported for this provider), the collection city is not on Get Package's list, or Get Package answered with an HTTP error. On a carrier HTTP error message is Error Retrieving Quote and the carrier's error body is in data; when the call could not be made at all (DNS, timeout) message is the transport error verbatim. |
| 403 | License could not be resolved, is deactivated, or is a premium company without a premium-active user. |
| 422 | Parameter validation failed, or license_key/domain was missing (that one comes from hasLicense). Only reachable after the provider guard has passed. Laravel's {"message", "errors"} shape, not the v1 envelope. |
GET /shipping/create_shipping_promise
Ask Wolt Drive for a shipment promise — a priced, time-bounded offer for delivering from the license's venue to the address you supply.
A promise here is informational
This endpoint returns Wolt's promise so a storefront can show a price and a window. It is not handed to shipment creation: open_new_order creates its own promise internally and uses that one's shipment_promise_id. Nothing you get back here needs to be stored or replayed.
Parameters
Query
| Field | Type | Required | Description |
|---|---|---|---|
license_key | string | yes | See Authentication. |
domain | string | yes | See Authentication. |
city | string | yes | Dropoff city. |
street | string | yes | Dropoff street. |
street_number | string | no | Dropoff street number. Appended to street before it is sent to Wolt. |
The Wolt venue, the preparation-time strategy and the opening hours all come from the license's settings (venue_id, time_type, min_preparation_time_minutes or time_slot, opening_time, closing_time), not from the request.
Closed hours
When the license has both opening_time and closing_time set and the current Asia/Jerusalem time is outside that window, no call is made to Wolt and the response is a failure carrying data.details: "Delivery area will be closed at the requested delivery time.".
How the price is rewritten
Wolt returns price.amount in minor units. The handler divides it by 100 and rounds to 2 decimals, then applies the license's price_type:
price_type | Effect on data.price.amount |
|---|---|
| unset / anything else | The converted carrier amount. |
fix | Replaced by the license's fix_price. |
markup | The converted carrier amount plus the license's markup_price, rounded to 2 decimals. |
Example request
curl --location --get 'https://app.shipos.co.il/api/shipping/create_shipping_promise' \
--header 'Accept: application/json' \
--data-urlencode 'license_key={license_key}' \
--data-urlencode 'domain=your-store.example.com' \
--data-urlencode 'city=תל אביב' \
--data-urlencode 'street=הרצל' \
--data-urlencode 'street_number=10'// Node.js 18+ / browsers — no dependencies
const query = new URLSearchParams({
license_key: process.env.SHIPOS_LICENSE_KEY,
domain: process.env.SHIPOS_DOMAIN,
city: 'תל אביב',
street: 'הרצל',
street_number: '10',
})
const response = await fetch(
`https://app.shipos.co.il/api/shipping/create_shipping_promise?${query}`,
{ headers: { Accept: 'application/json' } },
)
const body = await response.json()
if (!body.success) {
// e.g. outside the venue's opening hours
throw new Error(`ShipOS v1: ${body.data.details ?? body.message}`)
}
console.log(body.data.price.amount)<?php
use Illuminate\Support\Facades\Http;
$body = Http::acceptJson()
->get('https://app.shipos.co.il/api/shipping/create_shipping_promise', [
'license_key' => config('services.shipos.license_key'),
'domain' => config('services.shipos.domain'),
'city' => 'תל אביב',
'street' => 'הרצל',
'street_number' => '10',
])
->json();
if (! ($body['success'] ?? false)) {
throw new RuntimeException('ShipOS v1: '.($body['data']['details'] ?? $body['message']));
}
logger()->info('Wolt promise', ['amount' => $body['data']['price']['amount']]);Example response
data is Wolt's shipment-promise object with price.amount rewritten as described above. The keys this application itself reads from it are id, pickup.options, dropoff.location.coordinates, dropoff.location.formatted_address, dropoff.options and price; the rest of the object is Wolt's and is passed through.
{
"data": {
"id": "...",
"price": { "amount": 24.9, "...": "..." },
"pickup": { "options": [] },
"dropoff": { "location": { "coordinates": {}, "formatted_address": "..." }, "options": [] }
},
"message": "",
"success": true,
"warning": ""
}Outside opening hours:
{
"data": { "details": "Delivery area will be closed at the requested delivery time." },
"message": "Error Retrieving Quote",
"success": false,
"warning": ""
}Errors
| Status | When |
|---|---|
| 400 | The license's carrier is not Wolt Drive (Not supported for this provider), the venue is outside its opening hours, or Wolt did not answer 201. message falls back to Error Retrieving Quote. |
| 403 | License could not be resolved, is deactivated, or is a premium company without a premium-active user. |
| 422 | Parameter validation failed, or license_key/domain was missing (that one comes from hasLicense). Only reachable after the provider guard has passed. Laravel's {"message", "errors"} shape, not the v1 envelope. |
GET /shipping/get_express_delivery_enabled
Report whether ZigZag Express 24 is switched on for this license. This reads the license's own enable_express_delivery setting; it makes no call to ZigZag and asks nothing about an address.
Parameters
Query
| Field | Type | Required | Description |
|---|---|---|---|
license_key | string | yes | See Authentication. |
domain | string | yes | See Authentication. |
No other parameters. The handler does not validate anything beyond what hasLicense requires.
Example request
curl --location --get 'https://app.shipos.co.il/api/shipping/get_express_delivery_enabled' \
--header 'Accept: application/json' \
--data-urlencode 'license_key={license_key}' \
--data-urlencode 'domain=your-store.example.com'// Node.js 18+ / browsers — no dependencies
const query = new URLSearchParams({
license_key: process.env.SHIPOS_LICENSE_KEY,
domain: process.env.SHIPOS_DOMAIN,
})
const response = await fetch(
`https://app.shipos.co.il/api/shipping/get_express_delivery_enabled?${query}`,
{ headers: { Accept: 'application/json' } },
)
const body = await response.json()
// Not enabled arrives as HTTP 400 with success: false
console.log(body.success ? 'express on' : body.message)<?php
use Illuminate\Support\Facades\Http;
$body = Http::acceptJson()
->get('https://app.shipos.co.il/api/shipping/get_express_delivery_enabled', [
'license_key' => config('services.shipos.license_key'),
'domain' => config('services.shipos.domain'),
])
->json();
logger()->info($body['success'] ? 'express on' : $body['message']);Example response
The carrier service's own { success, message } pair is placed inside data, and its message is copied to the envelope's message, so the two agree.
{
"data": { "success": true, "message": "Express Delivery service is enabled" },
"message": "Express Delivery service is enabled",
"success": true,
"warning": ""
}Not enabled — note this is HTTP 400, not a 200 carrying false:
{
"data": { "success": false, "message": "Express Delivery service is not enabled" },
"message": "Express Delivery service is not enabled",
"success": false,
"warning": ""
}Errors
| Status | When |
|---|---|
| 400 | The license's carrier is not ZigZag (Not supported for this provider), or the license's enable_express_delivery setting is off. |
| 403 | License could not be resolved, is deactivated, or is a premium company without a premium-active user. |
| 422 | license_key or domain was omitted — raised by hasLicense before the handler runs. Laravel's {"message", "errors"} shape, not the v1 envelope. |
GET /shipping/get_express_delivery_status
Ask whether one address is eligible for ZigZag Express 24. The address is joined into "{city}, {street} {street_number}" and put to ZigZag's IsRemoteArea operation, using the express_username / express_password from the license settings; a remote area is not eligible.
Parameters
Query
| Field | Type | Required | Description |
|---|---|---|---|
license_key | string | yes | See Authentication. |
domain | string | yes | See Authentication. |
city | string | yes | Destination city. |
street | string | yes | Destination street. |
street_number | string | no | Destination street number. |
Example request
curl --location --get 'https://app.shipos.co.il/api/shipping/get_express_delivery_status' \
--header 'Accept: application/json' \
--data-urlencode 'license_key={license_key}' \
--data-urlencode 'domain=your-store.example.com' \
--data-urlencode 'city=תל אביב' \
--data-urlencode 'street=הרצל' \
--data-urlencode 'street_number=10'// Node.js 18+ / browsers — no dependencies
const query = new URLSearchParams({
license_key: process.env.SHIPOS_LICENSE_KEY,
domain: process.env.SHIPOS_DOMAIN,
city: 'תל אביב',
street: 'הרצל',
street_number: '10',
})
const response = await fetch(
`https://app.shipos.co.il/api/shipping/get_express_delivery_status?${query}`,
{ headers: { Accept: 'application/json' } },
)
const body = await response.json()
// An ineligible address is a 400, not a 200 with a false flag
const eligible = body.success
console.log(eligible ? 'express available' : body.message)<?php
use Illuminate\Support\Facades\Http;
$body = Http::acceptJson()
->get('https://app.shipos.co.il/api/shipping/get_express_delivery_status', [
'license_key' => config('services.shipos.license_key'),
'domain' => config('services.shipos.domain'),
'city' => 'תל אביב',
'street' => 'הרצל',
'street_number' => '10',
])
->json();
logger()->info($body['success'] ? 'express available' : $body['message']);Example response
Eligible:
{
"data": { "success": true, "message": "This area is eligible for Express Delivery" },
"message": "This area is eligible for Express Delivery",
"success": true,
"warning": ""
}Not eligible — HTTP 400:
{
"data": { "success": false, "message": "This area is not eligible for Express Delivery" },
"message": "This area is not eligible for Express Delivery",
"success": false,
"warning": ""
}Errors
| Status | When |
|---|---|
| 400 | The license's carrier is not ZigZag (Not supported for this provider), the license's enable_express_delivery setting is off, or the address is in a remote area and therefore not eligible. |
| 403 | License could not be resolved, is deactivated, or is a premium company without a premium-active user. |
| 422 | Parameter validation failed, or license_key/domain was missing (that one comes from hasLicense). Only reachable after the provider guard has passed. Laravel's {"message", "errors"} shape, not the v1 envelope. |
v2 equivalent
None. API v2 has no same-day quote, shipment-promise or express-eligibility endpoint — its surface is shipments, batches, labels, orders, tracking, pickup points, licenses, reference data, webhooks and notifications. If you need these carrier services, you stay on v1 for them.