Skip to content

רישיונות ​

רישיון הוא חשבון אצל חברת שילוח. ה-key שלו הוא פרט ההזדהות שכל קריאת shipping ב-v1 נושאת. שלוש נקודות קצה עוסקות ברישיון עצמו: שתיים שתוסף קורא להן בזמן שסוחר מדביק מפתח במסך ההגדרות, ואחת שמתרגמת מפתח לחשבון שמאחוריו.

שלוש נקודות קצה, שני מודלי אימות שונים

check_license ו-activate_license יושבות תחת קידומת הנתיב license, שאין לה שום middleware — לא hasLicense, לא תיעוד בקשות ולא נרמול תשובות. המפתח שאתם שולחים אינו נבדק לפני שה-handler רץ; בדיקתו היא הפעולה. user-by-license יושבת תחת הקידומת shipping ולכן נמצאת מאחורי hasLicense כמו כל נקודת קצה אחרת בקבוצת shipping, כך שמפתח שאינו שמיש נדחה עם HTTP 403 עוד לפני שמגיעים ל-handler שלה.

נקודת קצהקידומתhasLicenseמפתח שגוי מתבטא כ
GET /license/check_licenselicenseלאבשום צורה — כיום HTTP 500 על כל קלט, ראו את תיבת הפגם הידוע להלן
GET /license/activate_licenselicenseלאHTTP 200, success: false
GET /shipping/user-by-licenseshippingכן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) הן חוזרות בעברית.

json
{
  "message": "שדה domain הוא חובה.",
  "errors": { "domain": ["שדה domain הוא חובה."] }
}

הסתעפו לפי סטטוס 422 ולפי מפתחות השדות שבתוך errors. לעולם אל תסתעפו לפי טקסט ההודעה.

שתי נקודות הקצה שתחת הקידומת license מוודאות את אותם שני שדות, ואינן מוודאות שום דבר נוסף:

שדהסוגחובהתיאור
domainstringכןהדומיין של החנות הקוראת.
license_keystringכןמפתח הרישיון שנבדק או מופעל.

domain נדרש אך אינו נאכף

הוולידציה דוחה בקשה שהושמט ממנה domain, אבל הבדיקה שהשוותה אותו ל-domain השמור של הרישיון מוערת (commented out) ב-CheckLicense. לכן מפתח עובר ולידציה מכל דומיין כיום. אל תתכננו סביב זה — שלחו את הדומיין האמיתי.


GET /license/check_license ​

בדיקת מפתח רישיון. בשימוש התוספים כדי לומר לסוחר אם המפתח שהדביק עובד, לפני שנשלח איתו משלוח.

מה נבדק בפועל, לפי הסדר:

  1. קיימת שורת רישיון עם ה-key הזה — אחרת error: "missing", message: "Invalid License Key".
  2. הרישיון פעיל (is_active) — אחרת error: "revoked", message: "License key has been deactivated".
  3. אם החברה של הרישיון מסומנת כפרימיום, הקורא חייב להיפתר למשתמש עם פרימיום פעיל — אחרת 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_keystringכןמפתח הרישיון שיש לבדוק.
domainstringכןהדומיין של החנות הקוראת.

בקשה לדוגמה ​

bash
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'
js
// 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
<?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), ואצל רוב הספקים הם מחרוזות ריקות.

json
{
  "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:

json
{
  "success": false,
  "error": "missing",
  "license": "invalid",
  "message": "Invalid License Key"
}

שדות התשובה ​

שדהסוגתיאור
successboolהאות הקובע.
errorstring | nullmissing (אין מפתח כזה), revoked (הושבת), invalid (דרישת הפרימיום אינה מתקיימת), או null בהצלחה.
licensestringvalid או invalid. עודף על success.
providerobjectשורת חברת השילוח של החברה של הרישיון. קיים בהצלחה בלבד.
messagestringטקסט קריא לאדם. חלק מהערכים מתורגמים, ולכן אל תסתעפו לפיו.

שגיאות ​

סטטוסמתי
500כל בקשה כיום, בין שהמפתח תקף, מומצא או שאינו קיים במסד הנתונים — ה-ArgumentCountError שלמעלה. זו אינה תוצאה מתועדת; זו התוצאה הנצפית היחידה.
200כל תוצאה מתועדת, הצלחה וכישלון כאחד — ההתנהגות המיועדת, בת-השגה רק לאחר שהקריאה תתוקן.
422license_key או domain חסרים ממחרוזת השאילתה. הגוף הוא ה-{ message, errors } הפשוט של Laravel, מתורגם — ראו את התיבה בראש הדף.

GET /license/activate_license ​

קשירת מפתח רישיון לדומיין הקורא וחתימת זמן ההתחלה שלו. זהו החצי השני של הדבקת מפתח על ידי סוחר: check_license מדווחת, activate_license רושמת.

מה היא עושה, בדיוק:

  1. מאתרת את הרישיון לפי key. לא נמצא → error: "missing", message: "Invalid License Key", HTTP 200.
  2. מגדירה את domain לערך שנשלח רק אם לרישיון אין דומיין עדיין. רישיון שכבר קשור שומר על הדומיין הקיים שלו והבקשה בכל זאת מצליחה.
  3. מגדירה את started_at לעכשיו — בכל קריאה, כולל קריאות חוזרות.
  4. שומרת.

ההפעלה אינה מבצעת שום ולידציה של הרישיון

אין בנתיב הזה בדיקת 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_keystringכןמפתח הרישיון שיש להפעיל.
domainstringכןהדומיין שיש לקשור, בשימוש רק כאשר לרישיון אין דומיין רשום.

בקשה לדוגמה ​

bash
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'
js
// 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
<?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']);

תשובה לדוגמה ​

json
{
  "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:

json
{
  "success": false,
  "error": "missing",
  "license": "invalid",
  "message": "Invalid License Key"
}

message היא המחרוזת הקבועה License key is valid בהצלחה; היא אינה מדווחת אם הדומיין נכתב או שהיה מוגדר כבר.

שגיאות ​

סטטוסמתי
200שתי התוצאות, הצלחה ומפתח לא מוכר.
422license_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_keystringכןמפתח הרישיון שיש לתרגם.
domainstringכןהדומיין של החנות הקוראת.
tokenstringלאSanctum personal access token. hasLicense קורא אותו בכל בקשה; הוא משנה את התוצאה רק כאשר החברה של הרישיון היא פרימיום, ואז מה שהבדיקה דורשת הוא token שנפתר למשתמש עם פרימיום פעיל.

בקשה לדוגמה ​

bash
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'
js
// 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
<?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']]);

תשובה לדוגמה ​

json
{
  "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. קראו את המפתח שברמה העליונה והתעלמו מהעותקים.

שדות התשובה ​

שדהסוגתיאור
licenseobjectשורת ה-licenses — 20 מפתחות. is_expired ו-is_expiring_soon הן תכונות מחושבות שהמודל תמיד מוסיף; is_active, started_at ו-expires_at עוברים cast. שניים מהמפתחות הם אובייקטים מקוננים שקיימים תמיד: company (שורת ה-companies השלמה, כולל available_licenses_count מחושב שיכול להיות שלילי) ו-customer (שורת ה-users השלמה של הבעלים שוב, שנושאת בעצמה shopify_sites).
userobjectשורת ה-users של הלקוח של הרישיון, פחות עמודות ההזדהות המוסתרות.
shopify_sitesarrayאתרי ה-Shopify המחוברים של הלקוח. ריק כשאין כאלה.

שגיאות ​

סטטוסמתי
403hasLicense דחה את המפתח — לא מוכר, הושבת, או חברת פרימיום ללא משתמש עם פרימיום פעיל. זה מה שמקבל קורא רגיל ששולח רק license_key ו-domain עבור רישיון פרימיום. הגוף הוא מעטפת ארבעת המפתחות הסטנדרטית עם success: false.
422license_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.