רישיונות
רישיון הוא חשבון אצל חברת שילוח. ה-key שלו הוא פרט ההזדהות שכל קריאת shipping ב-v1 נושאת. שלוש נקודות קצה עוסקות ברישיון עצמו: שתיים שתוסף קורא להן בזמן שסוחר מדביק מפתח במסך ההגדרות, ואחת שמתרגמת מפתח לחשבון שמאחוריו.
שלוש נקודות קצה, שני מודלי אימות שונים
check_license ו-activate_license יושבות תחת קידומת הנתיב license, שאין לה שום middleware — לא hasLicense, לא תיעוד בקשות ולא נרמול תשובות. המפתח שאתם שולחים אינו נבדק לפני שה-handler רץ; בדיקתו היא הפעולה. user-by-license יושבת תחת הקידומת shipping ולכן נמצאת מאחורי hasLicense כמו כל נקודת קצה אחרת בקבוצת shipping, כך שמפתח שאינו שמיש נדחה עם HTTP 403 עוד לפני שמגיעים ל-handler שלה.
| נקודת קצה | קידומת | hasLicense | מפתח שגוי מתבטא כ |
|---|---|---|---|
GET /license/check_license | license | לא | בשום צורה — כיום HTTP 500 על כל קלט, ראו את תיבת הפגם הידוע להלן |
GET /license/activate_license | license | לא | HTTP 200, success: false |
GET /shipping/user-by-license | shipping | כן | HTTP 403, success: false |
המשמעות המעשית: אף אחת מהשתיים הראשונות אינה מחזירה, לפי התכנון, סטטוס שאינו 200, ולכן קורא חייב להסתעף לפי success ולא לפי הסטטוס. עם זאת, check_license מחזירה בקוד הנוכחי 500 קשה על כל מה שתשלחו לה — ראו את תיבת הפגם הידוע להלן; activate_license מתנהגת כמתועד. השלישית מתנהגת כמו שאר קבוצת ה-shipping.
מעטפת שונה
אף אחת מהשלוש אינה מחזירה את מעטפת ארבעת המפתחות { data, message, success, warning } שבשימוש שאר v1. check_license ו-activate_license מחזירות { success, error, license, provider, message }; user-by-license מחזירה { license, user, shopify_sites } בלי מפתח success כלל.
כישלוני ולידציה אינם משתמשים במעטפת v1
כל שלוש נקודות הקצה מחזירות את ה-payload של Laravel עצמו, { message, errors }, בכישלון ולידציה — לא את מעטפת ארבעת המפתחות, וגם לא את המבנה { success, error, ... } שלמעלה. ההודעות מתורגמות: במופעים הישראליים (APP_LOCALE=he) הן חוזרות בעברית.
{
"message": "שדה domain הוא חובה.",
"errors": { "domain": ["שדה domain הוא חובה."] }
}הסתעפו לפי סטטוס 422 ולפי מפתחות השדות שבתוך errors. לעולם אל תסתעפו לפי טקסט ההודעה.
שתי נקודות הקצה שתחת הקידומת license מוודאות את אותם שני שדות, ואינן מוודאות שום דבר נוסף:
| שדה | סוג | חובה | תיאור |
|---|---|---|---|
domain | string | כן | הדומיין של החנות הקוראת. |
license_key | string | כן | מפתח הרישיון שנבדק או מופעל. |
domain נדרש אך אינו נאכף
הוולידציה דוחה בקשה שהושמט ממנה domain, אבל הבדיקה שהשוותה אותו ל-domain השמור של הרישיון מוערת (commented out) ב-CheckLicense. לכן מפתח עובר ולידציה מכל דומיין כיום. אל תתכננו סביב זה — שלחו את הדומיין האמיתי.
GET /license/check_license
בדיקת מפתח רישיון. בשימוש התוספים כדי לומר לסוחר אם המפתח שהדביק עובד, לפני שנשלח איתו משלוח.
מה נבדק בפועל, לפי הסדר:
- קיימת שורת רישיון עם ה-
keyהזה — אחרתerror: "missing",message: "Invalid License Key". - הרישיון פעיל (
is_active) — אחרתerror: "revoked",message: "License key has been deactivated". - אם החברה של הרישיון מסומנת כפרימיום, הקורא חייב להיפתר למשתמש עם פרימיום פעיל — אחרת
error: "invalid"עםPremium companies are not supported in this version of plugin(כשאין משתמש בכלל) אוYou must be a premium member to use this company.
פקיעת תוקף אינה חלק מהבדיקה הזו, וגם לא הדומיין (ראו למעלה).
פגם ידוע
checkLicense קוראת ל-CheckLicense::check() עם שני ארגומנטים בזמן שה-action הזה מצהיר על שלושה פרמטרים נדרשים, ולכן בקוד הנוכחי הקריאה מעלה ArgumentCountError לפני שאף אחת מהבדיקות שלמעלה רצה — נקודת הקצה מחזירה 500 של ה-framework, ולא את המעטפת המתועדת להלן. מבני התשובה שכאן הם מה שה-handler נכתב להפיק; התייחסו לנקודת הקצה כבלתי שמישה עד שהקריאה תתוקן. activate_license ו-user-by-license אינן מושפעות.
פרמטרים
שאילתה
| שדה | סוג | חובה | תיאור |
|---|---|---|---|
license_key | string | כן | מפתח הרישיון שיש לבדוק. |
domain | string | כן | הדומיין של החנות הקוראת. |
בקשה לדוגמה
curl --location --get 'https://app.shipos.co.il/api/license/check_license' \
--header 'Accept: application/json' \
--data-urlencode 'license_key={license_key}' \
--data-urlencode 'domain=your-store.example.com'// Node.js 18+ / דפדפנים — בלי תלויות
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/license/check_license?${query}`,
{ headers: { Accept: 'application/json' } },
)
// לאחר תיקון הפגם: תמיד HTTP 200 — הסתעפו לפי success, לא לפי הסטטוס
const body = await response.json()
if (!body.success) {
throw new Error(`ShipOS v1: ${body.error} — ${body.message}`)
}<?php
use Illuminate\Support\Facades\Http;
$body = Http::acceptJson()
->get('https://app.shipos.co.il/api/license/check_license', [
'license_key' => config('services.shipos.license_key'),
'domain' => config('services.shipos.domain'),
])
->json();
if (! ($body['success'] ?? false)) {
throw new RuntimeException('ShipOS v1: '.$body['error'].' — '.$body['message']);
}תשובה לדוגמה
מפתח תקף. provider היא שורת ה-providers השלמה של החברה של הרישיון: id, name, endpoints, created_at ו-updated_at, ולא יותר מזה — אין בה שום דבר רגיש.
endpoints אינו אוסף של כתובות ה-API של חברת השילוח עצמה. הערכים הם שברי נתיב יחסיים שמצורפים ל-service_url של החברה של הרישיון (companies.service_url), ואצל רוב הספקים הם מחרוזות ריקות.
{
"success": true,
"error": null,
"license": "valid",
"provider": {
"id": 5,
"name": "Cargo",
"endpoints": { "shipping": "Webservice/", "label": "Webservice/", "tracking": "" },
"created_at": "2023-02-16T07:14:18.000000Z",
"updated_at": "2023-02-16T07:14:18.000000Z"
},
"message": "License key is valid"
}מפתח שאינו שמיש — עדיין HTTP 200, ובלי מפתח provider:
{
"success": false,
"error": "missing",
"license": "invalid",
"message": "Invalid License Key"
}שדות התשובה
| שדה | סוג | תיאור |
|---|---|---|
success | bool | האות הקובע. |
error | string | null | missing (אין מפתח כזה), revoked (הושבת), invalid (דרישת הפרימיום אינה מתקיימת), או null בהצלחה. |
license | string | valid או invalid. עודף על success. |
provider | object | שורת חברת השילוח של החברה של הרישיון. קיים בהצלחה בלבד. |
message | string | טקסט קריא לאדם. חלק מהערכים מתורגמים, ולכן אל תסתעפו לפיו. |
שגיאות
| סטטוס | מתי |
|---|---|
| 500 | כל בקשה כיום, בין שהמפתח תקף, מומצא או שאינו קיים במסד הנתונים — ה-ArgumentCountError שלמעלה. זו אינה תוצאה מתועדת; זו התוצאה הנצפית היחידה. |
| 200 | כל תוצאה מתועדת, הצלחה וכישלון כאחד — ההתנהגות המיועדת, בת-השגה רק לאחר שהקריאה תתוקן. |
| 422 | license_key או domain חסרים ממחרוזת השאילתה. הגוף הוא ה-{ message, errors } הפשוט של Laravel, מתורגם — ראו את התיבה בראש הדף. |
GET /license/activate_license
קשירת מפתח רישיון לדומיין הקורא וחתימת זמן ההתחלה שלו. זהו החצי השני של הדבקת מפתח על ידי סוחר: check_license מדווחת, activate_license רושמת.
מה היא עושה, בדיוק:
- מאתרת את הרישיון לפי
key. לא נמצא →error: "missing",message: "Invalid License Key", HTTP200. - מגדירה את
domainלערך שנשלח רק אם לרישיון אין דומיין עדיין. רישיון שכבר קשור שומר על הדומיין הקיים שלו והבקשה בכל זאת מצליחה. - מגדירה את
started_atלעכשיו — בכל קריאה, כולל קריאות חוזרות. - שומרת.
ההפעלה אינה מבצעת שום ולידציה של הרישיון
אין בנתיב הזה בדיקת is_active, אין בדיקת פקיעת תוקף ואין בדיקת פרימיום — אלה יושבות ב-check_license וב-hasLicense. מפתח שהושבת או שפג תוקפו מופעל כאן בהצלחה ואז נכשל בקריאת ה-shipping הראשונה. קראו ל-check_license אם אתם צריכים לדעת אם מפתח שמיש.
השמירה מפעילה את ה-hook License::updated, שמבצע POST יוצא סינכרוני אחד לכל אתר WooCommerce מחובר, בתוך הבקשה שלכם (wp-json/wc/v3/shipos_license_update_all). שתי השלכות, שתיהן נבדקו במופע חי על חשבון עם שני אתרים מחוברים: זמן התגובה של הבקשה גדל עם מספר האתרים המחוברים, ואתר שאי אפשר להגיע אליו נכשל בשקט — אחד מהשניים נכשל בפתירת DNS והכישלון נבלע ל-Log::info בזמן שהקורא בכל זאת קיבל תשובת הצלחה. ההפעלה אינה, אם כן, בדיקה לקריאה בלבד: אל תקראו לה כדי לבדוק מפתח, ואל תקראו לה בלופ.
הכתיבה הזו אינה משאירה שום שורה ב-api_request_logs. לקידומת הנתיב license אין middleware של תיעוד, ולכן לשינוי המצב שהיא מבצעת אין שם עקבות ביקורת.
עוד אושר במופע חי: domain שכבר מוגדר אינו נדרס גם כאשר נשלח דומיין אחר, ו-started_at עובר מ-null לעכשיו בכל קריאה מוצלחת.
פרמטרים
שאילתה
| שדה | סוג | חובה | תיאור |
|---|---|---|---|
license_key | string | כן | מפתח הרישיון שיש להפעיל. |
domain | string | כן | הדומיין שיש לקשור, בשימוש רק כאשר לרישיון אין דומיין רשום. |
בקשה לדוגמה
curl --location --get 'https://app.shipos.co.il/api/license/activate_license' \
--header 'Accept: application/json' \
--data-urlencode 'license_key={license_key}' \
--data-urlencode 'domain=your-store.example.com'// Node.js 18+ / דפדפנים — בלי תלויות
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/license/activate_license?${query}`,
{ headers: { Accept: 'application/json' } },
)
const body = await response.json()
if (!body.success) {
throw new Error(`ShipOS v1: ${body.message}`)
}
console.log(body.provider.name)<?php
use Illuminate\Support\Facades\Http;
$body = Http::acceptJson()
->get('https://app.shipos.co.il/api/license/activate_license', [
'license_key' => config('services.shipos.license_key'),
'domain' => config('services.shipos.domain'),
])
->json();
if (! ($body['success'] ?? false)) {
throw new RuntimeException('ShipOS v1: '.$body['message']);
}
logger()->info('Activated on '.$body['provider']['name']);תשובה לדוגמה
{
"success": true,
"error": null,
"license": "valid",
"provider": {
"id": 5,
"name": "Cargo",
"endpoints": { "shipping": "Webservice/", "label": "Webservice/", "tracking": "" },
"created_at": "2023-02-16T07:14:18.000000Z",
"updated_at": "2023-02-16T07:14:18.000000Z"
},
"message": "License key is valid"
}מפתח לא מוכר — HTTP 200:
{
"success": false,
"error": "missing",
"license": "invalid",
"message": "Invalid License Key"
}message היא המחרוזת הקבועה License key is valid בהצלחה; היא אינה מדווחת אם הדומיין נכתב או שהיה מוגדר כבר.
שגיאות
| סטטוס | מתי |
|---|---|
| 200 | שתי התוצאות, הצלחה ומפתח לא מוכר. |
| 422 | license_key או domain חסרים ממחרוזת השאילתה. הגוף הוא ה-{ message, errors } הפשוט של Laravel, מתורגם — ראו את התיבה בראש הדף. |
GET /shipping/user-by-license
תרגום מפתח רישיון לרישיון, ללקוח שבבעלותו הוא נמצא, ולאתרי ה-Shopify המחוברים של אותו לקוח. בשימוש ממשקים פנימיים (first-party) שמחזיקים מפתח וצריכים את החשבון שמאחוריו.
מכיוון שנקודת קצה זו נמצאת תחת הקידומת shipping, hasLicense כבר אימת את המפתח עד שה-handler רץ, ומפתח שאינו שמיש הפיק 403 עם מעטפת ארבעת המפתחות הסטנדרטית. ראו אימות.
נקודת הקצה הזו מחזירה את פרטי ההזדהות ל-API של החשבון
התשובה מטמיעה את שורת הרישיון המלאה ואת שורת ה-users המלאה של הלקוח הבעלים — 48 מפתחות במופע החי. password, remember_token ועמודות הדו-שלבי באמת אינן שם, המודל מסתיר אותן. כל השאר נמצא, ובכלל זה:
client_idו-client_secret— צמד פרטי ההזדהות ל-API של v2, אותם ערכים שקורא v2 שולח ב-X-Client-Id/X-Client-Secret.google_token,google_refresh_token,google_api_key,geoapify_api_key— פרטי הזדהות לשירותי צד שלישי שמוחזקים עבור החשבון.api_capabilitiesו-payment.
כל מי שמחזיק מפתח רישיון של v1 יכול לקרוא מנקודת הקצה הזו את פרטי ההזדהות ל-API של v2 של אותו חשבון ואת מפתחות ה-API שלו לצד שלישי. ברישיון שמולו נבדק הדף הזה, client_id ו-client_secret היו במקרה null בזמן ש-google_api_key הכיל מפתח חי — אבל השדות תמיד קיימים ב-payload, ולכן כל חשבון שהונפקו לו פרטי הזדהות ל-v2 חושף אותם כאן.
אותו גוף גם נכתב למסד הנתונים: התשובה המלאה, כולל ה-key של הרישיון, נשמרת מילה במילה ב-api_request_logs.normalized_response, ולכן הסודות האלה מגיעים גם לטבלת התיעוד.
לעולם אל תעבירו את התשובה הזו כפי שהיא לדפדפן, והתייחסו לכל עותק שמור שלה — כולל שורת התיעוד — כאל סוד.
חברות פרימיום: 403 לקורא רגיל, 500 מעבר לשער הזה
רישיון פרימיום שנשלח עם שני הפרמטרים המתועדים בלבד אינו מגיע ל-handler בכלל. hasLicense דוחה אותו עם HTTP 403 ועם message Premium companies are not supported in this version of plugin. זה מה שקורא רגיל רואה בפועל.
קורא שעובר מעבר ל-hasLicense כמשתמש עם פרימיום פעיל — כלומר כזה ששולח token שנפתר למשתמש כזה — נתקל בבעיה השנייה: ה-handler מריץ מחדש את בדיקת הרישיון בלי משתמש, אף ש-hasLicense בדיוק הריץ אותה עם משתמש. עבור חברת פרימיום הבדיקה השנייה הזו נכשלת, ולכן לא חוזר ממנה שום רישיון, וה-handler אז קורא את הלקוח מתוך רישיון שאינו קיים — הבקשה מסתיימת ב-500. הענף הזה נגזר מהקוד ולא שוחזר דרך HTTP. רישיונות שאינם פרימיום אינם מושפעים כך או כך.
פרמטרים
שאילתה
| שדה | סוג | חובה | תיאור |
|---|---|---|---|
license_key | string | כן | מפתח הרישיון שיש לתרגם. |
domain | string | כן | הדומיין של החנות הקוראת. |
token | string | לא | Sanctum personal access token. hasLicense קורא אותו בכל בקשה; הוא משנה את התוצאה רק כאשר החברה של הרישיון היא פרימיום, ואז מה שהבדיקה דורשת הוא token שנפתר למשתמש עם פרימיום פעיל. |
בקשה לדוגמה
curl --location --get 'https://app.shipos.co.il/api/shipping/user-by-license' \
--header 'Accept: application/json' \
--data-urlencode 'license_key={license_key}' \
--data-urlencode 'domain=your-store.example.com'// Node.js 18+ / דפדפנים — בלי תלויות
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/user-by-license?${query}`,
{ headers: { Accept: 'application/json' } },
)
if (!response.ok) {
const { message } = await response.json() // מעטפת 403
throw new Error(`ShipOS v1: ${message}`)
}
// אין מפתח success בתשובה הזו — 200 הוא אות ההצלחה
const { license, user, shopify_sites: shopifySites } = await response.json()
console.log(user.email, license.company_id, shopifySites.length)<?php
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->get('https://app.shipos.co.il/api/shipping/user-by-license', [
'license_key' => config('services.shipos.license_key'),
'domain' => config('services.shipos.domain'),
]);
if ($response->failed()) {
throw new RuntimeException('ShipOS v1: '.$response->json('message'));
}
$body = $response->json();
logger()->info('License owner', ['user_id' => $body['user']['id']]);תשובה לדוגמה
{
"license": {
"id": 812,
"key": "{license_key}",
"name": "Cargo — main store",
"domain": "your-store.example.com",
"is_active": true,
"type_id": 2,
"company_id": 3391,
"user_id": 5501,
"created_by": 1,
"commitment_day": 3,
"multiple_shipment": 0,
"started_at": "2026-01-31T00:00:00.000000Z",
"expires_at": "2027-01-31T00:00:00.000000Z",
"created_at": "2026-01-31T00:00:00.000000Z",
"updated_at": "2026-07-01T10:22:14.000000Z",
"deleted_at": null,
"is_expired": false,
"is_expiring_soon": false,
"company": {
"id": 3391,
"name": "...",
"service_url": "https://carrier.example.co.il/api/",
"provider_id": 5,
"is_premium": 0,
"license_quota": 10,
"available_licenses_count": -49,
"...": "..."
},
"customer": {
"id": 5501,
"name": "...",
"email": "...",
"client_id": null,
"client_secret": null,
"google_api_key": "...",
"shopify_sites": [ { "...": "..." } ],
"...": "..."
}
},
"user": {
"id": 5501,
"name": "...",
"email": "...",
"client_id": null,
"client_secret": null,
"google_api_key": "...",
"shopify_sites": [ { "...": "..." } ],
"...": "..."
},
"shopify_sites": [ { "...": "..." } ]
}לאובייקט license יש 20 מפתחות, ושניים מהם הם אובייקטים מקוננים שלמים. זה דטרמיניסטי ולא מקרי: ה-middleware hasLicense נוגע ב-$license->company וה-handler נוגע ב-$license->customer->shopify_sites, ולכן כל שלושת היחסים נטענים ומסודרים ל-JSON בכל קריאה מוצלחת.
license.companyהיא שורת ה-companiesהשלמה —service_url,provider_id,is_premium,license_quotaוכל השאר.available_licenses_countהיא תכונה מחושבת והיא יכולה להיות שלילית: הערך-49נצפה בחשבון חי שהנפיק רישיונות הרבה מעבר ל-license_quotaשלו. הגבילו אותה לאפס לפני שאתם מציגים אותה כ"רישיונות שנותרו".license.customerהיא שורת המשתמש הבעלים במלואה, שוב, בשכפול של המפתחuserברמה העליונה — כולל שדות פרטי ההזדהות, ראו את התיבה למעלה — והיא נושאתshopify_sitesמקונן משל עצמה.- לכן
shopify_sitesמופיע שלוש פעמים בתשובה אחת: כמפתח ברמה העליונה, על אובייקט ה-userברמה העליונה, ושוב עלlicense.customer. קראו את המפתח שברמה העליונה והתעלמו מהעותקים.
שדות התשובה
| שדה | סוג | תיאור |
|---|---|---|
license | object | שורת ה-licenses — 20 מפתחות. is_expired ו-is_expiring_soon הן תכונות מחושבות שהמודל תמיד מוסיף; is_active, started_at ו-expires_at עוברים cast. שניים מהמפתחות הם אובייקטים מקוננים שקיימים תמיד: company (שורת ה-companies השלמה, כולל available_licenses_count מחושב שיכול להיות שלילי) ו-customer (שורת ה-users השלמה של הבעלים שוב, שנושאת בעצמה shopify_sites). |
user | object | שורת ה-users של הלקוח של הרישיון, פחות עמודות ההזדהות המוסתרות. |
shopify_sites | array | אתרי ה-Shopify המחוברים של הלקוח. ריק כשאין כאלה. |
שגיאות
| סטטוס | מתי |
|---|---|
| 403 | hasLicense דחה את המפתח — לא מוכר, הושבת, או חברת פרימיום ללא משתמש עם פרימיום פעיל. זה מה שמקבל קורא רגיל ששולח רק license_key ו-domain עבור רישיון פרימיום. הגוף הוא מעטפת ארבעת המפתחות הסטנדרטית עם success: false. |
| 422 | license_key או domain חסרים. הגוף הוא ה-{ message, errors } הפשוט של Laravel, מתורגם — ראו את התיבה בראש הדף. |
| 500 | החברה של הרישיון מסומנת כפרימיום וגם הקורא עבר את hasLicense כמשתמש עם פרימיום פעיל, למשל על ידי שליחת token (ראו את האזהרה למעלה). נגזר מהקוד — לא שוחזר דרך HTTP; קורא רגיל מקבל את ה-403 במקום. |
המקבילה ב-v2
GET /licenses מחזירה כל רישיון שבבעלות הלקוח המאומת, עם החברה, חברת השילוח וההגדרות לכל ספק — והיא מחזירה את ה-key של הרישיון, ולכן היא מחליפה את user-by-license במקרה הנפוץ של "איזה חשבון זה?". היא מאמתת בכותרות X-Client-Id / X-Client-Secret ולא במפתח בתוך ה-payload.
אין מקבילה ב-v2 ל-check_license או ל-activate_license: ב-v2 אין מושג של הפעלת מפתח מול דומיין, ופרט הזדהות שגוי הוא פשוט 401.