משתמשי אפליקציה
שלוש נקודות קצה משרתות את אפליקציית הנמענים של ShipOS — האפליקציה לצרכן שאדם שמקבל חבילה משתמש בה כדי לראות איפה היא. הן רושמות נמען לפי מספר טלפון, מחזירות Sanctum token, ומחזירות את המשלוחים הממוענים לאותו טלפון.
לא לאינטגרציות של סוחרים
הקבוצה הזו משרתת את אפליקציית הנמענים הפנימית (first-party), לא חנויות. אין לה license_key, אין domain ואין מושג של חשבון אצל חברת שילוח: הגורם הפועל הוא נמען, המזוהה לפי מספר טלפון, והשער הוא סוד משותף שמוחזק בבילד של האפליקציה. רוב המשלבים לעולם לא יקראו לאלה. אם אתם שולחים חבילות, אתם רוצים את משלוחים; אם אתם מציגים מעקב לקונה, אתם רוצים את משטח המעקב, לא את זה.
אימות
כל route בקבוצה נמצא מאחורי needs_app_secret, שהוא השוואת כותרת בודדת:
| Header | חובה | תיאור |
|---|---|---|
X-App-Secret | כן | חייב להיות זהה לסוד האפליקציה המוגדר בשרת (API_SECRET בסביבת השרת). אין ערך לכל לקוח ושום דבר אינו נגזר מהבקשה — ה-middleware משווה את הכותרת לאותה מחרוזת אחת. |
כותרת חסרה או שגויה עוצרת את הבקשה ב-middleware:
{
"errors": [],
"message": "Invalid Secret Key",
"success": false
}סוד שגוי מחזיר HTTP 500
הדחייה משתמשת ב-helper הכישלון הגנרי של האפליקציה הזו, שסטטוס ברירת המחדל שלו הוא 500. זה לא 401 ולא 403. הסתעפו לפי success, ולא לפי מחלקת הסטטוס. ב-GET /shipping/phone_number תראו את התשובה הזו רק אחרי שכבר יש בידיכם bearer token תקף — ראו את הערת הסדר למטה.
GET /shipping/phone_number דורשת בנוסף auth:sanctum — token מסוג Bearer, מעל סוד האפליקציה. ה-token הזה הוא זה ששתי נקודות הקצה האחרות מנפיקות.
בדיקת auth:sanctum רצה לפני בדיקת סוד האפליקציה
הפקודה route:list מדפיסה את ה-middleware בסדר needs_app_secret ואחר כך auth:sanctum, אבל רשימת עדיפות ה-middleware של ברירת המחדל ב-Laravel מכילה את Illuminate\Auth\Middleware\Authenticate, ולכן האימות ממוין לפני NeedsAppSecret שאינו מתועדף — בלי קשר לסדר המודפס. ב-GET /shipping/phone_number זה הופך את סדר האבחון שהייתם מניחים: תגלו שהסוד שגוי רק אחרי שכבר יש בידיכם token תקף:
| בקשה | התשובה בפועל |
|---|---|
| סוד שגוי, בלי bearer token | 401 {"message":"Unauthenticated."} — מבנה Laravel החשוף, בלי מפתח success |
| סוד שגוי, עם bearer token תקף | 500 {"errors":[],"message":"Invalid Secret Key","success":false} |
| סוד נכון, בלי bearer token | 401 {"message":"Unauthenticated."} |
שתי מעטפות שונות
הצלחות משתמשות במעטפת ארבעת המפתחות { data, message, success, warning }. כישלונות בקבוצה הזו משתמשים במבנה שלושת המפתחות של helper הכישלון, { errors, message, success } — אין data ואין מפתח warning בכישלון כאן. תשובת 401 מ-auth:sanctum אינה משתמשת באף אחת מהשתיים: זהו המבנה של Laravel עצמו, {"message":"Unauthenticated."}, בלי מפתח success כלל.
הודעות המסגרת חוזרות בשפת השרת
לקבוצת ה-routes הזו אין middleware של localization, ולכן ל-Accept-Language אין השפעה (נבדק עם Accept-Language: en). כל message וכל רשומת errors שנוצרים על ידי המסגרת מגיעים בשפת השרת — עברית — למשל "שדה phone number הוא חובה." ולא "The phone number field is required.". לעולם אל תתאימו לפי טקסט ההודעה: הסתעפו לפי סטטוס ה-HTTP ולפי מפתחות השדות שבתוך errors.
GET /get_app_user
איתור נמען לפי מספר טלפון והנפקת token למכשיר הקורא. זו ההתחברות של אפליקציית הנמענים: אין סיסמה, ומה שמסמיך אותה הוא החזקת סוד האפליקציה בתוספת מספר טלפון מוכר.
token נוצר בכל קריאה מוצלחת, ושמו נגזר מ-device_name. tokens שהונפקו קודם אינם מבוטלים.
פרמטרים
שאילתה
| שדה | סוג | חובה | תיאור |
|---|---|---|---|
phone_number | string | כן | מספר הטלפון של הנמען, מושווה בדיוק מול ה-phone השמור. |
device_name | string | כן | שם ל-personal access token שנוצר. עד 255 תווים. |
בקשה לדוגמה
curl --location --get 'https://app.shipos.co.il/api/get_app_user' \
--header 'X-App-Secret: {app_secret}' \
--header 'Accept: application/json' \
--data-urlencode 'phone_number=0501234567' \
--data-urlencode 'device_name=Dana iPhone 15'// Node.js 18+ / דפדפנים — בלי תלויות
const query = new URLSearchParams({
phone_number: '0501234567',
device_name: 'Dana iPhone 15',
})
const response = await fetch(
`https://app.shipos.co.il/api/get_app_user?${query}`,
{
headers: {
'X-App-Secret': process.env.SHIPOS_APP_SECRET,
Accept: 'application/json',
},
},
)
const body = await response.json()
if (!body.success) {
throw new Error(`ShipOS v1: ${body.message}`) // "User not found"
}
const token = body.data.token // השתמשו בו כ-Bearer ב-shipping/phone_number<?php
use Illuminate\Support\Facades\Http;
$body = Http::withHeaders(['X-App-Secret' => config('services.shipos.app_secret')])
->acceptJson()
->get('https://app.shipos.co.il/api/get_app_user', [
'phone_number' => '0501234567',
'device_name' => 'Dana iPhone 15',
])
->json();
if (! ($body['success'] ?? false)) {
throw new RuntimeException('ShipOS v1: '.$body['message']);
}
$token = $body['data']['token'];תשובה לדוגמה
{
"data": {
"id": 4412,
"name": "ישראל ישראלי",
"phone": "0501234567",
"created_at": "2026-03-04T09:12:00.000000Z",
"updated_at": "2026-03-04T09:12:00.000000Z",
"token": "412|k7Yt..."
},
"message": "",
"success": true,
"warning": ""
}שדות התשובה
| שדה | סוג | תיאור |
|---|---|---|
id | integer | מזהה הנמען. |
name | string | שם הנמען. |
phone | string | מספר הטלפון של הנמען. ייחודי בין הנמענים. |
created_at | string | חותמת זמן בפורמט ISO 8601. |
updated_at | string | חותמת זמן בפורמט ISO 8601. |
token | string | Sanctum personal access token בטקסט גלוי, מוצג רק בתשובה הזו. שלחו אותו כ-Authorization: Bearer …. |
שגיאות
| סטטוס | מתי |
|---|---|
| 422 | phone_number או device_name חסרים, או ש-device_name ארוך מ-255 תווים. message הוא הודעת הוולידציה הראשונה; errors הוא המפה המלאה לפי שדות. |
| 500 | אין נמען עם מספר הטלפון הזה (message: "User not found", errors: []), או X-App-Secret שגוי (message: "Invalid Secret Key"). |
POST /create_app_user
רישום נמען והנפקת ה-token הראשון למכשיר הקורא. נקודת הקצה היחידה בקבוצה שמקבלת גוף בקשה.
פרמטרים
גוף הבקשה
| שדה | סוג | חובה | תיאור |
|---|---|---|---|
name | string | כן | שם הנמען. עד 255 תווים. |
phone_number | string | כן | מספר הטלפון של הנמען. חייב להיות ייחודי בין הנמענים — רישום טלפון שכבר קיים הוא כישלון ולידציה, ולא התחברות. עד 255 תווים. נשמר כ-phone. |
device_name | string | כן | שם ל-personal access token שנוצר. עד 255 תווים. |
בקשה לדוגמה
curl --location 'https://app.shipos.co.il/api/create_app_user' \
--header 'X-App-Secret: {app_secret}' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data '{
"name": "ישראל ישראלי",
"phone_number": "0501234567",
"device_name": "Dana iPhone 15"
}'// Node.js 18+ / דפדפנים — בלי תלויות
const response = await fetch('https://app.shipos.co.il/api/create_app_user', {
method: 'POST',
headers: {
'X-App-Secret': process.env.SHIPOS_APP_SECRET,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
name: 'ישראל ישראלי',
phone_number: '0501234567',
device_name: 'Dana iPhone 15',
}),
})
const body = await response.json()
if (!body.success) {
// 422 כאשר מספר הטלפון כבר רשום
throw new Error(`ShipOS v1: ${body.message}`)
}
const token = body.data.token<?php
use Illuminate\Support\Facades\Http;
$body = Http::withHeaders(['X-App-Secret' => config('services.shipos.app_secret')])
->acceptJson()
->post('https://app.shipos.co.il/api/create_app_user', [
'name' => 'ישראל ישראלי',
'phone_number' => '0501234567',
'device_name' => 'Dana iPhone 15',
])
->json();
if (! ($body['success'] ?? false)) {
throw new RuntimeException('ShipOS v1: '.$body['message']);
}
$token = $body['data']['token'];תשובה לדוגמה
זהה במבנה ל-get_app_user.
{
"data": {
"id": 4413,
"name": "ישראל ישראלי",
"phone": "0501234567",
"created_at": "2026-08-31T08:40:11.000000Z",
"updated_at": "2026-08-31T08:40:11.000000Z",
"token": "413|9pQx..."
},
"message": "",
"success": true,
"warning": ""
}כישלון ולידציה — כאן phone_number חסר. שימו לב שההודעה היא בשפת השרת, ולא בשפה שלכם:
{
"errors": {
"phone_number": ["שדה phone number הוא חובה."]
},
"message": "שדה phone number הוא חובה.",
"success": false
}שגיאות
| סטטוס | מתי |
|---|---|
| 422 | שדה חסר, מעל 255 תווים, או phone_number שכבר קיים. |
| 500 | X-App-Secret שגוי או חסר (message: "Invalid Secret Key"). |
GET /shipping/phone_number
רשימה מעומדת של המשלוחים הממוענים למספר הטלפון של הנמען המאומת. על אף הנתיב, אין פרמטר טלפון — הטלפון מגיע מה-token.
דורשת גם X-App-Secret וגם Authorization: Bearer {token}, כאשר ה-token הוא כזה שהונפק על ידי get_app_user או create_app_user. המשלוחים מותאמים לפי עמודת ה-phone של המשלוח עצמו מול ה-phone של בעל ה-token.
פרמטרים
שאילתה
| שדה | סוג | חובה | תיאור |
|---|---|---|---|
per_page | integer | לא | גודל העמוד. ברירת המחדל היא 100. |
page | integer | לא | מספר העמוד, פרמטר ה-paginator הסטנדרטי של Laravel. |
search | string | לא | התאמת תת-מחרוזת על השם הפרטי/שם המשפחה של הנמען, שורות הכתובת, העיר וקוד המשלוח, ובנוסף שם הלקוח של ההזמנה, מזהה ההזמנה ומספר ההזמנה. |
order_id | string | לא | התאמת תת-מחרוזת על ה-order_id או ה-order_number של ההזמנה. |
name | string | לא | התאמת תת-מחרוזת על השם הפרטי או שם המשפחה שבמשלוח. |
address | string | לא | התאמת תת-מחרוזת על שורת כתובת 1, שורת כתובת 2 או עיר. |
phone | string | לא | התאמת תת-מחרוזת על עמודת ה-phone של המשלוח עצמו או על ה-JSON של billing שבהזמנה. מצמצם בתוך המשלוחים של הנמען עצמו; אינו יכול להרחיב את הקבוצה. |
shipping_code | string | לא | התאמת תת-מחרוזת על קוד המעקב של חברת השילוח. |
search_by | object | לא | צורה מקוננת של חמישה מהמסננים שלמעלה, שמכובדת במקביל אליהם: search_by[order_id], search_by[name], search_by[address], search_by[phone], search_by[shipping_code]. כל אחד מתנהג כמו המקבילה השטוחה שלו, וערך ריק או falsy מדולג. |
customer | integer | לא | הגבלה למשלוחים שההזמנה שלהם שייכת למזהה הלקוח הזה. |
company | integer | לא | הגבלה למשלוחים שהרישיון של ההזמנה שלהם שייך למזהה החברה הזה. |
license | integer | לא | הגבלה למשלוחים על מזהה הרישיון הזה — מותאם מול ה-license_id של המשלוח עצמו או מול זה של ההזמנה שלו. |
flag | integer | לא | הגבלה למשלוחים הנושאים את מזהה הדגל הזה. |
type | string | לא | cod מגביל למשלוחים בגוביינא (shipment_data.is_cod אמיתי); independent מגביל למשלוחים על ספק Independent; כל ערך אחר מותאם מול עמודת ה-type של המשלוח ובנוסף מוציא משלוחים בגוביינא. |
sort_by_warehouse | string | לא | asc או desc, כשברירת המחדל היא desc. ממיין את התוצאות לפי מיקום במחסן, ומכובד רק כאשר לחשבון המאומת מופעל מיון לפי מחסן. |
status | string | לא | התאמת תת-מחרוזת על טקסט הסטטוס השמור של המשלוח. |
from | string | לא | רק משלוחים שנוצרו בתאריך הזה או אחריו. |
to | string | לא | רק משלוחים שנוצרו בתאריך הזה או לפניו. |
active_only | boolean | לא | כאשר true, מגביל למשלוחים שבהם is_active מוגדר — כלומר כאלה שלא בוטלו. |
label_generated | boolean | לא | סינון לפי האם נוצרה תווית. |
sort_by | string | לא | העמודה שלפיה למיין, מוסמכת מול טבלת ה-shippings. חייבת להישלח יחד עם sort_direction. |
sort_direction | string | לא | asc או desc. חייב להישלח יחד עם sort_by. |
בלי sort_by / sort_direction, התוצאות חוזרות מהחדשות לישנות (shippings.created_at בסדר יורד).
בקשה לדוגמה
curl --location --get 'https://app.shipos.co.il/api/shipping/phone_number' \
--header 'X-App-Secret: {app_secret}' \
--header 'Authorization: Bearer {token}' \
--header 'Accept: application/json' \
--data-urlencode 'per_page=20'// Node.js 18+ / דפדפנים — בלי תלויות
const query = new URLSearchParams({ per_page: '20' })
const response = await fetch(
`https://app.shipos.co.il/api/shipping/phone_number?${query}`,
{
headers: {
'X-App-Secret': process.env.SHIPOS_APP_SECRET,
Authorization: `Bearer ${token}`,
Accept: 'application/json',
},
},
)
const body = await response.json()
if (!body.success) {
throw new Error(`ShipOS v1: ${body.message}`)
}
console.log(body.data.client, body.data.meta.total)
for (const shipment of body.data.shippings) {
console.log(shipment.shipping_id, shipment.shipping_status)
}<?php
use Illuminate\Support\Facades\Http;
$body = Http::withHeaders(['X-App-Secret' => config('services.shipos.app_secret')])
->withToken($token)
->acceptJson()
->get('https://app.shipos.co.il/api/shipping/phone_number', [
'per_page' => 20,
])
->json();
if (! ($body['success'] ?? false)) {
throw new RuntimeException('ShipOS v1: '.$body['message']);
}
foreach ($body['data']['shippings'] as $shipment) {
logger()->info($shipment['shipping_id'].' '.$shipment['shipping_status']);
}תשובה לדוגמה
{
"data": {
"client": "ישראל ישראלי",
"shippings": [
{
"orderId": "M85157304",
"shipping_id": "171258",
"shipping_status": "פעיל",
"shipping_date": "2026-07-07T13:44:04.000000Z",
"shipping_company": "Lionwheel test",
"shipping_from": null,
"shipping_from_logo": "https://app.shipos.co.il/storage/logos/lionwheel-test.png",
"items": []
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"total": 3,
"last_page": 1,
"from": 1,
"to": 3,
"path": "https://app.shipos.co.il/api/shipping/phone_number",
"first_page_url": "...",
"last_page_url": "...",
"next_page_url": null,
"prev_page_url": null,
"links": [ { "...": "..." } ]
}
},
"message": "",
"success": true,
"warning": ""
}שדות התשובה
| שדה | סוג | תיאור |
|---|---|---|
client | string | ה-name של הנמען המאומת. |
shippings | array | עמוד המשלוחים הנוכחי, כל אחד במבנה של ShippingResource — בדיוק שמונה מפתחות, המפורטים למטה. |
meta | object | ה-payload של ה-paginator של Laravel פחות מפתח ה-data שלו — current_page, per_page, total, last_page, from, to, path, first_page_url, last_page_url, next_page_url, prev_page_url, links. |
כל רשומה ב-shippings:
| שדה | סוג | תיאור |
|---|---|---|
orderId | string | האסמכתא של הסוחר להזמנה — ה-order_id של ההזמנה, למשל "M85157304". |
shipping_id | string | מזהה המשלוח, כמחרוזת. זהו מזהה השורה ב-ShipOS, ולא קוד המעקב של חברת השילוח. |
shipping_status | string | טקסט הסטטוס, בשפת השרת — למשל "פעיל". |
shipping_date | string | חותמת זמן היצירה בפורמט ISO 8601. |
shipping_company | string | שם התצוגה של חברת השילוח שאיתה יצא המשלוח. |
shipping_from | string | null | תווית השולח / נקודת האיסוף. null במשלוחים שאין להם כזו. |
shipping_from_logo | string | כתובת URL מלאה ל-storage של ShipOS עבור הלוגו. |
items | array | פריטי השורה של המשלוח. [] כאשר אין למשלוח מוצרים. |
כל רשומה ב-items נושאת את item_name, item_price, item_price_formatted, item_quantity, item_total, item_total_formatted, item_image ו-item_currency.
shippings הוא המבנה של ShippingResource
ה-handler בונה אוסף ShippingResource בשורה אחת ומחזיר את $shippings->items() בשורה שאחריה. השורה הראשונה נראית כמו קוד מת, אבל היא נושאת משקל: ShippingResource::collection($shippings) משנה את ה-paginator במקום — ה-collectResource() של Laravel קורא ל-setCollection() — ולכן items() מחזיר את מופעי ה-resource, ולא את שורות המודל. לכן לכל רשומה יש בדיוק שמונת המפתחות שלמעלה, זהים בכל 137 השורות שנבדקו בשרת החי. אין בתשובה id, uuid, first_name, city, order או products בשום מקום.
שגיאות
| סטטוס | מתי |
|---|---|
| 401 | token מסוג Authorization: Bearer חסר או שגוי. מכיוון ש-auth:sanctum ממוין לפני בדיקת הסוד, זו גם התשובה על X-App-Secret שגוי כל עוד ה-token חסר. מבנה Laravel החשוף — {"message":"Unauthenticated."}, בלי מפתח success. |
| 422 | per_page אינו integer. |
| 500 | X-App-Secret שגוי או חסר (message: "Invalid Secret Key") — נגיש רק אחרי שיש bearer token תקף. |
| 500 | Attempt to read property "currency" on null — פגם בצד השרת ב-ShippingResource, שנדרך על ידי משלוח בעמוד המבוקש שיש לו מוצרים אבל הזמנה null. ראו למטה. |
ברירת המחדל המתועדת עלולה להחזיר HTTP 500
per_page אכן מקבל כברירת מחדל את 100, אבל קריאה בלי שום פרמטר — ברירת המחדל המתועדת — החזירה 500 בשרת החי: "Attempt to read property \"currency\" on null", שנזרק מ-app/Http/Resources/Api/ShippingResource.php. שלוש שורות במיפוי ה-items קוראות את $this->order->currency בלי הגנה, בעוד השדות השכנים משתמשים נכון ב-?->, ולכן משלוח בודד שיש לו מוצרים אבל הזמנה מנותקת (null) מפיל את כל העמוד. זהו פגם ידוע בצד השרת, ולא שגיאה של הקורא: האם תיתקלו בו תלוי בנתונים של הנמען, ולא בבקשה שלכם. בחיפוש בינארי בשרת החי על נמען אחד, per_page=7 החזיר 200 ו-per_page=8 החזיר 500 — כלומר per_page קטן יכול לעקוף שורה בעייתית. הפגם לא תוקן.
המקבילה ב-v2
אין. API v2 מאמת סוחר באמצעות client credentials ותוחם את הכול לרישיונות של אותו סוחר; אין לו זהות של נמען, אין התחברות לפי מספר טלפון ואין שער של סוד אפליקציה. אין ב-v2 שום דבר להעביר אליו את שלוש נקודות הקצה האלה.