Skip to content

קבלת webhooks

במקום לבצע polling ל-GET /shipments/{shipment}/status, רשמו URL למינוי ותנו ל-ShipOS לדחוף אליכם אירועי JSON חתומים ברגע שהם קורים. מדריך זה לוקח אתכם מהמינוי ועד למקלט מוכן-לפרודקשן ובטוח מפני replay. פירוט endpoint אחרי endpoint נמצא בייחוס Webhooks.

שלב 1 — מינוי

צרו מינוי עם POST /webhooks, עם כתובת ה-HTTPS למשלוח האירועים והאירועים שאתם רוצים. האירועים הזמינים למינוי הם:

אירוענורה כאשר
shipment.createdמשלוח נוצר.
shipment.status_changedסטטוס משלוח משתנה.
shipment.deliveredמשלוח נמסר.
shipment.cancelledמשלוח מבוטל.

Webhooks מוגבלים לרישיון (license-scoped), לכן כללו license_key אם בבעלות החשבון שלכם יותר מרישיון פעיל אחד (ראו מדריך רישיונות).

bash
curl --location 'https://app.shipos.co.il/api/v2/webhooks' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "license_key": "{license_key}",
  "url": "https://example.com/hooks/shipos",
  "events": ["shipment.created", "shipment.status_changed", "shipment.delivered", "shipment.cancelled"]
}'
js
const res = await fetch('https://app.shipos.co.il/api/v2/webhooks', {
  method: 'POST',
  headers: {
    'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
    'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
    'Accept': 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    license_key: process.env.SHIPOS_LICENSE_KEY,
    url: 'https://example.com/hooks/shipos',
    events: ['shipment.created', 'shipment.status_changed', 'shipment.delivered', 'shipment.cancelled'],
  }),
});

const { data } = await res.json();
const secret = data.secret; // whsec_… — שמרו אותו עכשיו, מוצג פעם אחת בלבד
php
<?php
// composer require guzzlehttp/guzzle

$client = new \GuzzleHttp\Client([
    'base_uri' => 'https://app.shipos.co.il/api/v2/',
    'headers' => [
        'X-Client-Id' => getenv('SHIPOS_CLIENT_ID'),
        'X-Client-Secret' => getenv('SHIPOS_CLIENT_SECRET'),
        'Accept' => 'application/json',
    ],
]);

$response = $client->post('webhooks', ['json' => [
    'license_key' => getenv('SHIPOS_LICENSE_KEY'),
    'url' => 'https://example.com/hooks/shipos',
    'events' => ['shipment.created', 'shipment.status_changed', 'shipment.delivered', 'shipment.cancelled'],
]]);

$data = json_decode($response->getBody()->getContents(), true)['data'];

echo $data['secret'], PHP_EOL; // whsec_… — שמרו אותו עכשיו, מוצג פעם אחת בלבד
php
<?php

use Illuminate\Support\Facades\Http;

$response = Http::withHeaders([
    'X-Client-Id' => config('services.shipos.client_id'),
    'X-Client-Secret' => config('services.shipos.client_secret'),
])
    ->acceptJson()
    ->post('https://app.shipos.co.il/api/v2/webhooks', [
        'license_key' => config('services.shipos.license_key'),
        'url' => 'https://example.com/hooks/shipos',
        'events' => ['shipment.created', 'shipment.status_changed', 'shipment.delivered', 'shipment.cancelled'],
    ])
    ->throw();

$secret = $response->json('data.secret'); // whsec_… — שמרו אותו עכשיו, מוצג פעם אחת בלבד
python
# pip install httpx
import os

import httpx

response = httpx.post(
    "https://app.shipos.co.il/api/v2/webhooks",
    headers={
        "X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
        "X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
        "Accept": "application/json",
    },
    json={
        "license_key": os.environ["SHIPOS_LICENSE_KEY"],
        "url": "https://example.com/hooks/shipos",
        "events": [
            "shipment.created",
            "shipment.status_changed",
            "shipment.delivered",
            "shipment.cancelled",
        ],
    },
)
response.raise_for_status()

secret = response.json()["data"]["secret"]  # whsec_… — שמרו אותו עכשיו, מוצג פעם אחת בלבד
go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	body, _ := json.Marshal(map[string]any{
		"license_key": os.Getenv("SHIPOS_LICENSE_KEY"),
		"url":         "https://example.com/hooks/shipos",
		"events": []string{"shipment.created", "shipment.status_changed",
			"shipment.delivered", "shipment.cancelled"},
	})

	req, _ := http.NewRequest("POST", "https://app.shipos.co.il/api/v2/webhooks", bytes.NewReader(body))
	req.Header.Set("X-Client-Id", os.Getenv("SHIPOS_CLIENT_ID"))
	req.Header.Set("X-Client-Secret", os.Getenv("SHIPOS_CLIENT_SECRET"))
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var payload struct {
		Data struct {
			ID     string `json:"id"`
			Secret string `json:"secret"`
		} `json:"data"`
	}
	if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
		panic(err)
	}

	// whsec_… — שמרו אותו עכשיו, מוצג פעם אחת בלבד
	fmt.Println(payload.Data.ID, payload.Data.Secret)
}
java
// Java 17+ — java.net.http, ללא תלויות (פענוח עם Jackson/Gson)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class CreateShipOsWebhook {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "license_key": "%s",
              "url": "https://example.com/hooks/shipos",
              "events": ["shipment.created", "shipment.status_changed", "shipment.delivered", "shipment.cancelled"]
            }
            """.formatted(System.getenv("SHIPOS_LICENSE_KEY"));

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.shipos.co.il/api/v2/webhooks"))
            .header("X-Client-Id", System.getenv("SHIPOS_CLIENT_ID"))
            .header("X-Client-Secret", System.getenv("SHIPOS_CLIENT_SECRET"))
            .header("Accept", "application/json")
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        // data.secret הוא whsec_… — שמרו אותו עכשיו, מוצג פעם אחת בלבד
        System.out.println(response.body());
    }
}
csharp
// .NET 8+ — System.Net.Http.Json
using System.Net.Http.Json;
using System.Text.Json;

using var http = new HttpClient
{
    BaseAddress = new Uri("https://app.shipos.co.il/api/v2/"),
};
http.DefaultRequestHeaders.Add("X-Client-Id",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_ID"));
http.DefaultRequestHeaders.Add("X-Client-Secret",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_SECRET"));

var response = await http.PostAsJsonAsync("webhooks", new
{
    license_key = Environment.GetEnvironmentVariable("SHIPOS_LICENSE_KEY"),
    url = "https://example.com/hooks/shipos",
    events = new[]
    {
        "shipment.created", "shipment.status_changed",
        "shipment.delivered", "shipment.cancelled",
    },
});
response.EnsureSuccessStatusCode();

var data = (await response.Content.ReadFromJsonAsync<JsonDocument>())!
    .RootElement.GetProperty("data");

// whsec_… — שמרו אותו עכשיו, מוצג פעם אחת בלבד
Console.WriteLine(data.GetProperty("secret").GetString());
ruby
require "net/http"
require "json"

uri = URI("https://app.shipos.co.il/api/v2/webhooks")
request = Net::HTTP::Post.new(uri)
request["X-Client-Id"] = ENV.fetch("SHIPOS_CLIENT_ID")
request["X-Client-Secret"] = ENV.fetch("SHIPOS_CLIENT_SECRET")
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
request.body = JSON.generate(
  license_key: ENV.fetch("SHIPOS_LICENSE_KEY"),
  url: "https://example.com/hooks/shipos",
  events: ["shipment.created", "shipment.status_changed", "shipment.delivered", "shipment.cancelled"]
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
raise "ShipOS error: #{response.body}" unless response.is_a?(Net::HTTPSuccess)

data = JSON.parse(response.body).fetch("data")
secret = data["secret"] # whsec_… — שמרו אותו עכשיו, מוצג פעם אחת בלבד
rust
// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let payload: Value = reqwest::Client::new()
        .post("https://app.shipos.co.il/api/v2/webhooks")
        .header("X-Client-Id", std::env::var("SHIPOS_CLIENT_ID")?)
        .header("X-Client-Secret", std::env::var("SHIPOS_CLIENT_SECRET")?)
        .header("Accept", "application/json")
        .json(&json!({
            "license_key": std::env::var("SHIPOS_LICENSE_KEY")?,
            "url": "https://example.com/hooks/shipos",
            "events": ["shipment.created", "shipment.status_changed",
                       "shipment.delivered", "shipment.cancelled"],
        }))
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;

    // whsec_… — שמרו אותו עכשיו, מוצג פעם אחת בלבד
    println!("{}", payload["data"]["secret"]);
    Ok(())
}

תגובת ה-201 כוללת את המינוי — ורק בתגובת היצירה הזו — את ה-secret לחתימה:

json
{
  "data": {
    "id": "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f",
    "url": "https://example.com/hooks/shipos",
    "events": ["shipment.created", "shipment.status_changed", "shipment.delivered", "shipment.cancelled"],
    "is_active": true,
    "secret": "whsec_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "created_at": "2026-07-29T10:00:00.000000Z",
    "updated_at": "2026-07-29T10:00:00.000000Z"
  }
}

סכנה — שמרו את ה-secret עכשיו

ה-secret בפורמט whsec_… הוא write-only. הוא מופיע בתגובת היצירה ולעולם לא שוב — לא ב-GET /webhooks, לא בעדכונים. אם איבדתם אותו, מחקו את המינוי וצרו חדש.

שלב 2 — אימות חתימות

כל delivery הוא HTTP POST עם שני headers:

  • X-ShipOS-Event — שם האירוע (למשל shipment.created).
  • X-ShipOS-Signature — בפורמט t={timestamp},v1={hmac}.

ה-{hmac} הוא HMAC-SHA256 בקידוד hex על המחרוזת "{timestamp}.{body}", עם ה-secret המלא שלכם בפורמט whsec_… כמפתח, כאשר {body} הוא גוף הבקשה הגולמי בדיוק כפי שהתקבל. הגוף עצמו נראה כך:

json
{
  "event": "shipment.created",
  "created_at": 1753783200,
  "data": {
    "id": "b7e4c9d2-1a3f-4e5b-9c8d-2f1e0a9b8c7d",
    "tracking_code": "66747921",
    "service_type": 1
  }
}

אמתו לפני שאתם סומכים על משהו:

  1. פצלו את ה-header ל-t ו-v1.
  2. חשבו HMAC-SHA256(secret, t + "." + rawBody).
  3. השוו מול v1 בהשוואה בטוחה מבחינת תזמון (timing-safe).
  4. אופציונלית, דחו ערכי t ישנים כדי להגביל חלונות replay (הסבילות היא בחירה שלכם; 5 דקות היא ברירת מחדל נפוצה — שימו לב שניסיונות חוזרים לגיטימיים משתמשים שוב בחותמת הזמן המקורית, לכן שמרו על חלון רחב לפחות כמו לוח הניסיונות החוזרים, בערך 2–3 דקות, או בצעו דה-דופליקציה במקום לדחות).
js
const crypto = require('node:crypto');
const express = require('express');

const app = express();
const SECRET = process.env.SHIPOS_WEBHOOK_SECRET; // whsec_…

function verifyShipOsSignature(header, rawBody, secret, toleranceSeconds = 300) {
  // פורמט ה-header: t={timestamp},v1={hmac}
  const parts = Object.fromEntries(
    header.split(',').map((pair) => pair.split('=', 2).map((s) => s.trim())),
  );

  if (!parts.t || !parts.v1 || !/^\d+$/.test(parts.t)) return false;

  // הגנת replay אופציונלית.
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);

  return a.length === b.length && crypto.timingSafeEqual(a, b); // בטוח מבחינת תזמון
}

// חשוב: אמתו מול הגוף הגולמי (RAW), לא מול אובייקט JSON שסודר מחדש.
app.post('/hooks/shipos', express.raw({ type: 'application/json' }), (req, res) => {
  const rawBody = req.body.toString('utf8');
  const header = req.get('X-ShipOS-Signature') || '';

  if (!verifyShipOsSignature(header, rawBody, SECRET)) {
    return res.status(400).send('invalid signature');
  }

  const event = JSON.parse(rawBody);
  // ... טפלו באירוע, ואז השיבו 2xx
  res.sendStatus(200);
});
php
<?php

function verifyShipOsSignature(string $header, string $rawBody, string $secret, int $toleranceSeconds = 300): bool
{
    // פורמט ה-header: t={timestamp},v1={hmac}
    $parts = [];
    foreach (explode(',', $header) as $pair) {
        [$key, $value] = array_pad(explode('=', $pair, 2), 2, '');
        $parts[trim($key)] = trim($value);
    }

    if (empty($parts['t']) || empty($parts['v1']) || ! ctype_digit($parts['t'])) {
        return false;
    }

    // הגנת replay אופציונלית.
    if (abs(time() - (int) $parts['t']) > $toleranceSeconds) {
        return false;
    }

    $expected = hash_hmac('sha256', $parts['t'].'.'.$rawBody, $secret);

    return hash_equals($expected, $parts['v1']); // בטוח מבחינת תזמון
}

// שימוש (endpoint ב-PHP רגיל) — php://input הוא הגוף הגולמי (RAW), לעולם לא מערך שקודד מחדש.
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_SHIPOS_SIGNATURE'] ?? '';
$secret = getenv('SHIPOS_WEBHOOK_SECRET'); // whsec_…

if (! verifyShipOsSignature($header, $rawBody, $secret)) {
    http_response_code(400);
    exit;
}

$event = json_decode($rawBody, true);
// ... טפלו ב-$event, ואז השיבו 2xx
http_response_code(200);
php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ShipOsWebhookController extends Controller
{
    public function __invoke(Request $request)
    {
        // getContent() הוא הגוף הגולמי (RAW) — לעולם אל תבצעו hash על מערך json() שקודד מחדש.
        $rawBody = $request->getContent();

        // פורמט ה-header: t={timestamp},v1={hmac}
        parse_str(str_replace(',', '&', (string) $request->header('X-ShipOS-Signature')), $parts);

        abort_if(empty($parts['t']) || empty($parts['v1']), 400);

        // הגנת replay אופציונלית.
        abort_if(abs(time() - (int) $parts['t']) > 300, 400);

        $expected = hash_hmac(
            'sha256',
            $parts['t'].'.'.$rawBody,
            config('services.shipos.webhook_secret'), // whsec_…
        );

        abort_unless(hash_equals($expected, $parts['v1']), 400); // בטוח מבחינת תזמון

        // ... שגרו job עם $request->json()->all(), ואז השיבו 2xx
        return response()->json(['received' => true]);
    }
}
python
# pip install flask
import hashlib
import hmac
import os
import time

from flask import Flask, request

app = Flask(__name__)
SECRET = os.environ["SHIPOS_WEBHOOK_SECRET"].encode()  # whsec_…


def verify_shipos_signature(header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    # פורמט ה-header: t={timestamp},v1={hmac}
    parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)

    if not parts.get("t", "").isdigit() or not parts.get("v1"):
        return False

    # הגנת replay אופציונלית.
    if abs(time.time() - int(parts["t"])) > tolerance:
        return False

    signed = parts["t"].encode() + b"." + raw_body  # בייטים גולמיים (RAW), לעולם לא dict שעבר dump מחדש
    expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()

    return hmac.compare_digest(expected, parts["v1"])  # בטוח מבחינת תזמון


@app.post("/hooks/shipos")
def shipos_hook():
    # request.get_data() מחזיר את הגוף הגולמי (RAW) בדיוק כפי שהתקבל.
    if not verify_shipos_signature(
        request.headers.get("X-ShipOS-Signature", ""), request.get_data()
    ):
        return "invalid signature", 400

    event = request.get_json()
    # ... טפלו באירוע, ואז השיבו 2xx
    return "", 200
go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

// rawBody חייב להיות בייטים גולמיים (RAW) של הבקשה — לעולם לא struct שעבר marshal מחדש.
func verifyShipOsSignature(header string, rawBody []byte, secret string) bool {
	// פורמט ה-header: t={timestamp},v1={hmac}
	parts := map[string]string{}
	for _, pair := range strings.Split(header, ",") {
		if key, value, ok := strings.Cut(pair, "="); ok {
			parts[strings.TrimSpace(key)] = strings.TrimSpace(value)
		}
	}

	ts, err := strconv.ParseInt(parts["t"], 10, 64)
	if err != nil || time.Since(time.Unix(ts, 0)).Abs() > 5*time.Minute {
		return false // timestamp שגוי, או חריגה מחלון ה-replay האופציונלי
	}

	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(parts["t"] + "."))
	mac.Write(rawBody)

	received, err := hex.DecodeString(parts["v1"])

	return err == nil && hmac.Equal(mac.Sum(nil), received) // בטוח מבחינת תזמון
}

func main() {
	secret := os.Getenv("SHIPOS_WEBHOOK_SECRET") // whsec_…

	http.HandleFunc("/hooks/shipos", func(w http.ResponseWriter, r *http.Request) {
		rawBody, _ := io.ReadAll(r.Body)

		if !verifyShipOsSignature(r.Header.Get("X-ShipOS-Signature"), rawBody, secret) {
			http.Error(w, "invalid signature", http.StatusBadRequest)

			return
		}

		// ... טפלו באירוע, ואז השיבו 2xx
		w.WriteHeader(http.StatusOK)
	})

	http.ListenAndServe(":8080", nil)
}
java
// Java 17+ — javax.crypto, ללא תלויות
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class ShipOsSignature {
    /** rawBody חייב להיות בייטים גולמיים (RAW) של הבקשה — לעולם לא אובייקט שסודר מחדש. */
    public static boolean verify(String header, byte[] rawBody) throws Exception {
        // פורמט ה-header: t={timestamp},v1={hmac}
        Map<String, String> parts = new HashMap<>();
        for (String pair : header.split(",")) {
            String[] kv = pair.split("=", 2);
            if (kv.length == 2) {
                parts.put(kv[0].trim(), kv[1].trim());
            }
        }

        if (!parts.containsKey("t") || !parts.containsKey("v1")) {
            return false;
        }

        long timestamp = Long.parseLong(parts.get("t"));
        if (Math.abs(Instant.now().getEpochSecond() - timestamp) > 300) {
            return false; // הגנת replay אופציונלית
        }

        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(
            System.getenv("SHIPOS_WEBHOOK_SECRET").getBytes(StandardCharsets.UTF_8),
            "HmacSHA256"));
        mac.update((parts.get("t") + ".").getBytes(StandardCharsets.UTF_8));
        mac.update(rawBody);

        // השוואה בטוחה מבחינת תזמון
        return MessageDigest.isEqual(mac.doFinal(), HexFormat.of().parseHex(parts.get("v1")));
    }
}
csharp
// .NET 8+ — System.Security.Cryptography
using System.Security.Cryptography;
using System.Text;

// rawBody חייב להיות בייטים גולמיים (RAW) של הבקשה — לעולם לא אובייקט שסודר מחדש.
static bool VerifyShipOsSignature(string header, byte[] rawBody, int toleranceSeconds = 300)
{
    // פורמט ה-header: t={timestamp},v1={hmac}
    var parts = header.Split(',')
        .Select(pair => pair.Split('=', 2))
        .Where(kv => kv.Length == 2)
        .ToDictionary(kv => kv[0].Trim(), kv => kv[1].Trim());

    if (!long.TryParse(parts.GetValueOrDefault("t"), out var timestamp)
        || !parts.TryGetValue("v1", out var received))
    {
        return false;
    }

    // הגנת replay אופציונלית.
    if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - timestamp) > toleranceSeconds)
    {
        return false;
    }

    var secret = Environment.GetEnvironmentVariable("SHIPOS_WEBHOOK_SECRET")!; // whsec_…
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));

    var signed = Encoding.UTF8.GetBytes($"{timestamp}.").Concat(rawBody).ToArray();

    return CryptographicOperations.FixedTimeEquals(
        hmac.ComputeHash(signed), Convert.FromHexString(received));
}
ruby
require "json"
require "openssl"
require "sinatra"

SECRET = ENV.fetch("SHIPOS_WEBHOOK_SECRET") # whsec_…

def verify_shipos_signature(header, raw_body, tolerance = 300)
  # פורמט ה-header: t={timestamp},v1={hmac}
  parts = header.to_s.split(",").map { |pair| pair.split("=", 2).map(&:strip) }.to_h

  return false unless parts["t"] =~ /\A\d+\z/ && parts["v1"]

  # הגנת replay אופציונלית.
  return false if (Time.now.to_i - parts["t"].to_i).abs > tolerance

  expected = OpenSSL::HMAC.hexdigest("SHA256", SECRET, "#{parts["t"]}.#{raw_body}")

  return false unless expected.bytesize == parts["v1"].bytesize

  OpenSSL.fixed_length_secure_compare(expected, parts["v1"]) # בטוח מבחינת תזמון
end

post "/hooks/shipos" do
  raw_body = request.body.read # הגוף הגולמי (RAW) — לעולם לא מחרוזת JSON שנוצרה מחדש
  unless verify_shipos_signature(request.env["HTTP_X_SHIPOS_SIGNATURE"], raw_body)
    halt 400, "invalid signature"
  end

  event = JSON.parse(raw_body)
  # ... טפלו באירוע, ואז השיבו 2xx
  status 200
end
rust
// [dependencies]
// hmac = "0.12"
// sha2 = "0.10"
// hex = "0.4"
use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::time::{SystemTime, UNIX_EPOCH};

/// `raw_body` חייב להיות בייטים גולמיים (RAW) של הבקשה — לעולם לא struct שסודר מחדש.
fn verify_shipos_signature(header: &str, raw_body: &[u8], secret: &str) -> bool {
    // פורמט ה-header: t={timestamp},v1={hmac}
    let (mut t, mut v1) = ("", "");
    for pair in header.split(',') {
        match pair.trim().split_once('=') {
            Some(("t", value)) => t = value,
            Some(("v1", value)) => v1 = value,
            _ => {}
        }
    }

    let (Ok(timestamp), Ok(signature)) = (t.parse::<i64>(), hex::decode(v1)) else {
        return false;
    };

    let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
    if (now - timestamp).abs() > 300 {
        return false; // הגנת replay אופציונלית
    }

    let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).expect("valid key");
    mac.update(t.as_bytes());
    mac.update(b".");
    mac.update(raw_body);

    mac.verify_slice(&signature).is_ok() // בטוח מבחינת תזמון
}

אזהרה — חתמו תמיד על הבייטים הגולמיים

חשבו את ה-HMAC על גוף הבקשה בדיוק כפי שהתקבל. פענוח ה-JSON וסדרתו מחדש ישנו את סדר המפתחות, את קידוד ה-unicode או את הרווחים — והחתימה לא תתאים.

שלב 3 — בדיקה עם ping

הכניסו לתור delivery בדיקה ל-endpoint שלכם:

bash
curl --location --request POST 'https://app.shipos.co.il/api/v2/webhooks/{uuid}/ping?license_key={license_key}' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json'
js
const uuid = '9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f';
const url = new URL(`https://app.shipos.co.il/api/v2/webhooks/${uuid}/ping`);
url.searchParams.set('license_key', process.env.SHIPOS_LICENSE_KEY);

const res = await fetch(url, {
  method: 'POST',
  headers: {
    'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
    'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
    'Accept': 'application/json',
  },
});

const { data } = await res.json();
console.log(data.queued); // true — נכנס לתור, לא שהמקבל קיבל אותו
php
<?php
// composer require guzzlehttp/guzzle

$uuid = '9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f';

$client = new \GuzzleHttp\Client([
    'base_uri' => 'https://app.shipos.co.il/api/v2/',
    'headers' => [
        'X-Client-Id' => getenv('SHIPOS_CLIENT_ID'),
        'X-Client-Secret' => getenv('SHIPOS_CLIENT_SECRET'),
        'Accept' => 'application/json',
    ],
]);

$response = $client->post("webhooks/{$uuid}/ping", [
    'query' => ['license_key' => getenv('SHIPOS_LICENSE_KEY')],
]);

$data = json_decode($response->getBody()->getContents(), true)['data'];

var_dump($data['queued']); // true
php
<?php

use Illuminate\Support\Facades\Http;

$uuid = '9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f';
$url = "https://app.shipos.co.il/api/v2/webhooks/{$uuid}/ping"
    .'?license_key='.config('services.shipos.license_key');

$queued = Http::withHeaders([
    'X-Client-Id' => config('services.shipos.client_id'),
    'X-Client-Secret' => config('services.shipos.client_secret'),
])
    ->acceptJson()
    ->post($url)
    ->throw()
    ->json('data.queued');

logger()->info('ping queued: '.var_export($queued, true));
python
# pip install httpx
import os

import httpx

uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f"

response = httpx.post(
    f"https://app.shipos.co.il/api/v2/webhooks/{uuid}/ping",
    params={"license_key": os.environ["SHIPOS_LICENSE_KEY"]},
    headers={
        "X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
        "X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
        "Accept": "application/json",
    },
)
response.raise_for_status()

print(response.json()["data"]["queued"])  # True
go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
)

func main() {
	uuid := "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f"
	query := url.Values{"license_key": {os.Getenv("SHIPOS_LICENSE_KEY")}}

	req, _ := http.NewRequest("POST",
		"https://app.shipos.co.il/api/v2/webhooks/"+uuid+"/ping?"+query.Encode(), nil)
	req.Header.Set("X-Client-Id", os.Getenv("SHIPOS_CLIENT_ID"))
	req.Header.Set("X-Client-Secret", os.Getenv("SHIPOS_CLIENT_SECRET"))
	req.Header.Set("Accept", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var payload struct {
		Data struct {
			Queued bool `json:"queued"`
		} `json:"data"`
	}
	if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
		panic(err)
	}

	fmt.Println(payload.Data.Queued) // true
}
java
// Java 17+ — java.net.http, ללא תלויות (פענוח עם Jackson/Gson)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class ShipOsPingWebhook {
    public static void main(String[] args) throws Exception {
        String uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f";

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.shipos.co.il/api/v2/webhooks/" + uuid
                + "/ping?license_key=" + System.getenv("SHIPOS_LICENSE_KEY")))
            .header("X-Client-Id", System.getenv("SHIPOS_CLIENT_ID"))
            .header("X-Client-Secret", System.getenv("SHIPOS_CLIENT_SECRET"))
            .header("Accept", "application/json")
            .POST(HttpRequest.BodyPublishers.noBody())
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new RuntimeException("ShipOS error: " + response.body());
        }

        System.out.println(response.body()); // {"data":{"queued":true}}
    }
}
csharp
// .NET 8+ — System.Net.Http.Json
using System.Net.Http.Json;
using System.Text.Json;

var uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f";
var licenseKey = Environment.GetEnvironmentVariable("SHIPOS_LICENSE_KEY");

using var http = new HttpClient
{
    BaseAddress = new Uri("https://app.shipos.co.il/api/v2/"),
};
http.DefaultRequestHeaders.Add("X-Client-Id",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_ID"));
http.DefaultRequestHeaders.Add("X-Client-Secret",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_SECRET"));

var response = await http.PostAsync($"webhooks/{uuid}/ping?license_key={licenseKey}", null);
response.EnsureSuccessStatusCode();

var payload = await response.Content.ReadFromJsonAsync<JsonDocument>()
    ?? throw new InvalidOperationException("Empty response");

Console.WriteLine(
    payload.RootElement.GetProperty("data").GetProperty("queued").GetBoolean());
ruby
require "net/http"
require "json"

uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f"
uri = URI("https://app.shipos.co.il/api/v2/webhooks/#{uuid}/ping")
uri.query = URI.encode_www_form(license_key: ENV.fetch("SHIPOS_LICENSE_KEY"))

request = Net::HTTP::Post.new(uri)
request["X-Client-Id"] = ENV.fetch("SHIPOS_CLIENT_ID")
request["X-Client-Secret"] = ENV.fetch("SHIPOS_CLIENT_SECRET")
request["Accept"] = "application/json"

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

raise "ShipOS error: #{response.body}" unless response.is_a?(Net::HTTPSuccess)

puts JSON.parse(response.body).dig("data", "queued") # true
rust
// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::Value;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f";

    let payload: Value = reqwest::Client::new()
        .post(format!("https://app.shipos.co.il/api/v2/webhooks/{uuid}/ping"))
        .query(&[("license_key", std::env::var("SHIPOS_LICENSE_KEY")?)])
        .header("X-Client-Id", std::env::var("SHIPOS_CLIENT_ID")?)
        .header("X-Client-Secret", std::env::var("SHIPOS_CLIENT_SECRET")?)
        .header("Accept", "application/json")
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;

    println!("{}", payload["data"]["queued"]); // true
    Ok(())
}

ה-API מגיב { "data": { "queued": true } } וה-endpoint שלכם מקבל זמן קצר לאחר מכן delivery חתום עם X-ShipOS-Event: ping והגוף הזה:

json
{
  "event": "ping",
  "created_at": 1753783200,
  "data": { "message": "This is a test event from ShipOS." }
}

ping הוא שם אירוע לבדיקות בלבד — הוא אינו חלק מרשימת האירועים הזמינים למינוי, ולכן ה-handler שלכם צריך לקבל אותו (או לפחות להחזיר עבורו 2xx) אף על פי שמעולם לא נרשמתם אליו.

שלב 4 — דיבוג deliveries

כל ניסיון delivery מתועד. בחנו אותם עם:

bash
curl --location 'https://app.shipos.co.il/api/v2/webhooks/{uuid}/deliveries?license_key={license_key}&per_page=25' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json'
js
const uuid = '9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f';
const url = new URL(`https://app.shipos.co.il/api/v2/webhooks/${uuid}/deliveries`);
url.searchParams.set('license_key', process.env.SHIPOS_LICENSE_KEY);
url.searchParams.set('per_page', '25');

const res = await fetch(url, {
  headers: {
    'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
    'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
    'Accept': 'application/json',
  },
});

const { data: deliveries } = await res.json();

for (const attempt of deliveries) {
  console.log(attempt.event, attempt.attempt, attempt.status_code, attempt.success);
}
php
<?php
// composer require guzzlehttp/guzzle

$uuid = '9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f';

$client = new \GuzzleHttp\Client([
    'base_uri' => 'https://app.shipos.co.il/api/v2/',
    'headers' => [
        'X-Client-Id' => getenv('SHIPOS_CLIENT_ID'),
        'X-Client-Secret' => getenv('SHIPOS_CLIENT_SECRET'),
        'Accept' => 'application/json',
    ],
]);

$response = $client->get("webhooks/{$uuid}/deliveries", [
    'query' => ['license_key' => getenv('SHIPOS_LICENSE_KEY'), 'per_page' => 25],
]);

$deliveries = json_decode($response->getBody()->getContents(), true)['data'];

foreach ($deliveries as $attempt) {
    echo $attempt['event'], ' #', $attempt['attempt'], ' ', $attempt['status_code'], PHP_EOL;
}
php
<?php

use Illuminate\Support\Facades\Http;

$uuid = '9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f';

$deliveries = Http::withHeaders([
    'X-Client-Id' => config('services.shipos.client_id'),
    'X-Client-Secret' => config('services.shipos.client_secret'),
])
    ->acceptJson()
    ->get("https://app.shipos.co.il/api/v2/webhooks/{$uuid}/deliveries", [
        'license_key' => config('services.shipos.license_key'),
        'per_page' => 25,
    ])
    ->throw()
    ->json('data');

foreach ($deliveries as $attempt) {
    logger()->info($attempt['event'].' #'.$attempt['attempt'].' '.$attempt['status_code']);
}
python
# pip install httpx
import os

import httpx

uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f"

response = httpx.get(
    f"https://app.shipos.co.il/api/v2/webhooks/{uuid}/deliveries",
    params={"license_key": os.environ["SHIPOS_LICENSE_KEY"], "per_page": 25},
    headers={
        "X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
        "X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
        "Accept": "application/json",
    },
)
response.raise_for_status()

for attempt in response.json()["data"]:
    print(attempt["event"], attempt["attempt"], attempt["status_code"], attempt["success"])
go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
)

type deliveryList struct {
	Data []struct {
		Event      string `json:"event"`
		Attempt    int    `json:"attempt"`
		StatusCode *int   `json:"status_code"`
		Success    bool   `json:"success"`
	} `json:"data"`
}

func main() {
	uuid := "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f"
	query := url.Values{
		"license_key": {os.Getenv("SHIPOS_LICENSE_KEY")},
		"per_page":    {"25"},
	}

	req, _ := http.NewRequest("GET",
		"https://app.shipos.co.il/api/v2/webhooks/"+uuid+"/deliveries?"+query.Encode(), nil)
	req.Header.Set("X-Client-Id", os.Getenv("SHIPOS_CLIENT_ID"))
	req.Header.Set("X-Client-Secret", os.Getenv("SHIPOS_CLIENT_SECRET"))
	req.Header.Set("Accept", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var list deliveryList
	if err := json.NewDecoder(res.Body).Decode(&list); err != nil {
		panic(err)
	}

	for _, attempt := range list.Data {
		fmt.Println(attempt.Event, attempt.Attempt, attempt.Success)
	}
}
java
// Java 17+ — java.net.http, ללא תלויות (פענוח עם Jackson/Gson)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class ShipOsWebhookDeliveries {
    public static void main(String[] args) throws Exception {
        String uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f";

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.shipos.co.il/api/v2/webhooks/" + uuid
                + "/deliveries?license_key=" + System.getenv("SHIPOS_LICENSE_KEY")
                + "&per_page=25"))
            .header("X-Client-Id", System.getenv("SHIPOS_CLIENT_ID"))
            .header("X-Client-Secret", System.getenv("SHIPOS_CLIENT_SECRET"))
            .header("Accept", "application/json")
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new RuntimeException("ShipOS error: " + response.body());
        }

        System.out.println(response.body()); // {"data":[...]} — למפו עם Jackson/Gson
    }
}
csharp
// .NET 8+ — System.Net.Http.Json
using System.Net.Http.Json;
using System.Text.Json;

var uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f";
var licenseKey = Environment.GetEnvironmentVariable("SHIPOS_LICENSE_KEY");

using var http = new HttpClient
{
    BaseAddress = new Uri("https://app.shipos.co.il/api/v2/"),
};
http.DefaultRequestHeaders.Add("X-Client-Id",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_ID"));
http.DefaultRequestHeaders.Add("X-Client-Secret",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_SECRET"));

var payload = await http.GetFromJsonAsync<JsonDocument>(
    $"webhooks/{uuid}/deliveries?license_key={licenseKey}&per_page=25")
    ?? throw new InvalidOperationException("Empty response");

foreach (var attempt in payload.RootElement.GetProperty("data").EnumerateArray())
{
    Console.WriteLine(
        $"{attempt.GetProperty("event").GetString()} #{attempt.GetProperty("attempt").GetInt32()} {attempt.GetProperty("success").GetBoolean()}");
}
ruby
require "net/http"
require "json"

uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f"
uri = URI("https://app.shipos.co.il/api/v2/webhooks/#{uuid}/deliveries")
uri.query = URI.encode_www_form(
  license_key: ENV.fetch("SHIPOS_LICENSE_KEY"), per_page: 25
)

request = Net::HTTP::Get.new(uri)
request["X-Client-Id"] = ENV.fetch("SHIPOS_CLIENT_ID")
request["X-Client-Secret"] = ENV.fetch("SHIPOS_CLIENT_SECRET")
request["Accept"] = "application/json"

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

raise "ShipOS error: #{response.body}" unless response.is_a?(Net::HTTPSuccess)

JSON.parse(response.body).fetch("data").each do |attempt|
  puts "#{attempt["event"]} ##{attempt["attempt"]} #{attempt["status_code"]} #{attempt["success"]}"
end
rust
// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::Value;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f";

    let payload: Value = reqwest::Client::new()
        .get(format!("https://app.shipos.co.il/api/v2/webhooks/{uuid}/deliveries"))
        .query(&[
            ("license_key", std::env::var("SHIPOS_LICENSE_KEY")?),
            ("per_page", "25".to_string()),
        ])
        .header("X-Client-Id", std::env::var("SHIPOS_CLIENT_ID")?)
        .header("X-Client-Secret", std::env::var("SHIPOS_CLIENT_SECRET")?)
        .header("Accept", "application/json")
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;

    for attempt in payload["data"].as_array().unwrap_or(&Vec::new()) {
        println!("{} {} {}", attempt["event"], attempt["attempt"], attempt["success"]);
    }
    Ok(())
}

התגובה מעומדת (ברירת המחדל של per_page היא 25, מקסימום 100). כל שורת delivery:

json
{
  "data": [
    {
      "id": 512,
      "event": "shipment.created",
      "attempt": 2,
      "status_code": 500,
      "success": false,
      "error": "HTTP 500",
      "created_at": "2026-07-29T10:00:35.000000Z",
      "updated_at": "2026-07-29T10:00:35.000000Z"
    }
  ]
}
שדהמשמעות
eventהאירוע שנשלח.
attemptאיזה ניסיון חוזר השורה מתעדת (1 = ניסיון ראשון).
status_codeסטטוס ה-HTTP שה-endpoint שלכם החזיר, או null בכשל תעבורה (timeout, DNS, TLS).
successtrue כאשר ה-endpoint שלכם החזיר 2xx.
errorHTTP {status} עבור תגובות שאינן 2xx, או הודעת שגיאת התעבורה; null בהצלחה.

שלב 5 — טיפול אידמפוטנטי בניסיונות חוזרים

delivery נחשב ככושל על כל תגובה שאינה 2xx או כשל תעבורה (הבקשה מסתיימת ב-timeout אחרי 15 שניות). deliveries שנכשלו מנוסים שוב עד 5 ניסיונות סך הכול, עם backoff של 30 שניות בין ניסיונות. שתי השלכות ל-handler שלכם:

  • השיבו 2xx מהר. עשו את המינימום (אימות חתימה, הכנסה לתור לעיבוד), ואז החזירו 200. אם העיבוד הכבד שלכם גורם לבקשה לחרוג מ-15 שניות, ShipOS מתעד כשל ושולח מחדש.
  • צפו לכפילויות. ניסיון חוזר שולח מחדש את הגוף והחתימה הזהים (אותה חותמת זמן created_at, אותו payload). וגם 2xx ש-ShipOS מעולם לא רואה (למשל חיבור שנפל) מפעיל שליחה מחדש. בצעו דה-דופליקציה לפני שאתם פועלים: מפתח טוב הוא hash של הגוף הגולמי, או השלשה (event, data.id, created_at). שמרו מפתחות שעובדו ודלגו על חזרות.
js
// דוגמה לדה-דופליקציה באמצעות hash של הגוף הגולמי
const deliveryKey = crypto.createHash('sha256').update(rawBody).digest('hex');
if (await alreadyProcessed(deliveryKey)) return res.sendStatus(200);
await markProcessed(deliveryKey);

טיפ

אירועים עשויים גם להגיע שלא לפי הסדר (משימות בתור, ניסיונות חוזרים). התייחסו ל-payloads של shipment.status_changed כטריגר לקריאה מחדש של המשלוח דרך GET /shipments/{shipment} ולא כמקור האמת לסדר האירועים.

שלב 6 — מחזור חיים: עדכון והסרה

שנו את ה-URL, את סט האירועים, או השהו deliveries עם PATCH חלקי — כל שדה הוא אופציונלי, וה-secret לחתימה לעולם אינו משתנה:

bash
# השהיית מינוי בלי למחוק אותו
curl --location --request PATCH 'https://app.shipos.co.il/api/v2/webhooks/{uuid}' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "license_key": "{license_key}",
  "is_active": false
}'

# שינוי ה-URL וסט האירועים
curl --location --request PATCH 'https://app.shipos.co.il/api/v2/webhooks/{uuid}' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "license_key": "{license_key}",
  "url": "https://example.com/hooks/shipos-v2",
  "events": ["shipment.delivered"]
}'
js
const uuid = '9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f';

const res = await fetch(`https://app.shipos.co.il/api/v2/webhooks/${uuid}`, {
  method: 'PATCH',
  headers: {
    'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
    'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
    'Accept': 'application/json',
    'Content-Type': 'application/json',
  },
  // שלחו `url` ו/או `events` באותו אופן כדי לשנות את היעד או את סט האירועים.
  body: JSON.stringify({
    license_key: process.env.SHIPOS_LICENSE_KEY,
    is_active: false,
  }),
});

const { data: webhook } = await res.json();
console.log(webhook.is_active, webhook.events.join(', '));
php
<?php
// composer require guzzlehttp/guzzle

$uuid = '9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f';

$client = new \GuzzleHttp\Client([
    'base_uri' => 'https://app.shipos.co.il/api/v2/',
    'headers' => [
        'X-Client-Id' => getenv('SHIPOS_CLIENT_ID'),
        'X-Client-Secret' => getenv('SHIPOS_CLIENT_SECRET'),
        'Accept' => 'application/json',
    ],
]);

// שלחו 'url' ו/או 'events' באותו אופן כדי לשנות את היעד או את סט האירועים.
$response = $client->patch("webhooks/{$uuid}", ['json' => [
    'license_key' => getenv('SHIPOS_LICENSE_KEY'),
    'is_active' => false,
]]);

$webhook = json_decode($response->getBody()->getContents(), true)['data'];

var_dump($webhook['is_active']);
php
<?php

use Illuminate\Support\Facades\Http;

$uuid = '9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f';

// שלחו 'url' ו/או 'events' באותו אופן כדי לשנות את היעד או את סט האירועים.
$webhook = Http::withHeaders([
    'X-Client-Id' => config('services.shipos.client_id'),
    'X-Client-Secret' => config('services.shipos.client_secret'),
])
    ->acceptJson()
    ->patch("https://app.shipos.co.il/api/v2/webhooks/{$uuid}", [
        'license_key' => config('services.shipos.license_key'),
        'is_active' => false,
    ])
    ->throw()
    ->json('data');

logger()->info('webhook paused: '.var_export($webhook['is_active'], true));
python
# pip install httpx
import os

import httpx

uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f"

# שלחו "url" ו/או "events" באותו אופן כדי לשנות את היעד או את סט האירועים.
response = httpx.patch(
    f"https://app.shipos.co.il/api/v2/webhooks/{uuid}",
    headers={
        "X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
        "X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
        "Accept": "application/json",
    },
    json={
        "license_key": os.environ["SHIPOS_LICENSE_KEY"],
        "is_active": False,
    },
)
response.raise_for_status()

print(response.json()["data"]["is_active"])  # False
go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	uuid := "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f"

	// שלחו "url" ו/או "events" באותו אופן כדי לשנות את היעד או את סט האירועים.
	body, _ := json.Marshal(map[string]any{
		"license_key": os.Getenv("SHIPOS_LICENSE_KEY"),
		"is_active":   false,
	})

	req, _ := http.NewRequest("PATCH",
		"https://app.shipos.co.il/api/v2/webhooks/"+uuid, bytes.NewReader(body))
	req.Header.Set("X-Client-Id", os.Getenv("SHIPOS_CLIENT_ID"))
	req.Header.Set("X-Client-Secret", os.Getenv("SHIPOS_CLIENT_SECRET"))
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var payload struct {
		Data struct {
			IsActive bool     `json:"is_active"`
			Events   []string `json:"events"`
		} `json:"data"`
	}
	if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
		panic(err)
	}

	fmt.Println(payload.Data.IsActive, payload.Data.Events)
}
java
// Java 17+ — java.net.http, ללא תלויות (פענוח עם Jackson/Gson)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class UpdateShipOsWebhook {
    public static void main(String[] args) throws Exception {
        String uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f";

        // שלחו "url" ו/או "events" באותו אופן כדי לשנות את היעד או את סט האירועים.
        String body = """
            { "license_key": "%s", "is_active": false }
            """.formatted(System.getenv("SHIPOS_LICENSE_KEY"));

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.shipos.co.il/api/v2/webhooks/" + uuid))
            .header("X-Client-Id", System.getenv("SHIPOS_CLIENT_ID"))
            .header("X-Client-Secret", System.getenv("SHIPOS_CLIENT_SECRET"))
            .header("Accept", "application/json")
            .header("Content-Type", "application/json")
            .method("PATCH", HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.body()); // {"data":{...,"is_active":false}}
    }
}
csharp
// .NET 8+ — System.Net.Http.Json
using System.Net.Http.Json;
using System.Text.Json;

var uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f";

using var http = new HttpClient
{
    BaseAddress = new Uri("https://app.shipos.co.il/api/v2/"),
};
http.DefaultRequestHeaders.Add("X-Client-Id",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_ID"));
http.DefaultRequestHeaders.Add("X-Client-Secret",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_SECRET"));

// שלחו url / events באותו אופן כדי לשנות את היעד או את סט האירועים.
var response = await http.PatchAsJsonAsync($"webhooks/{uuid}", new
{
    license_key = Environment.GetEnvironmentVariable("SHIPOS_LICENSE_KEY"),
    is_active = false,
});
response.EnsureSuccessStatusCode();

var payload = await response.Content.ReadFromJsonAsync<JsonDocument>()
    ?? throw new InvalidOperationException("Empty response");

Console.WriteLine(
    payload.RootElement.GetProperty("data").GetProperty("is_active").GetBoolean());
ruby
require "net/http"
require "json"

uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f"
uri = URI("https://app.shipos.co.il/api/v2/webhooks/#{uuid}")

request = Net::HTTP::Patch.new(uri)
request["X-Client-Id"] = ENV.fetch("SHIPOS_CLIENT_ID")
request["X-Client-Secret"] = ENV.fetch("SHIPOS_CLIENT_SECRET")
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
# שלחו :url ו/או :events באותו אופן כדי לשנות את היעד או את סט האירועים.
request.body = JSON.generate(
  license_key: ENV.fetch("SHIPOS_LICENSE_KEY"),
  is_active: false
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

raise "ShipOS error: #{response.body}" unless response.is_a?(Net::HTTPSuccess)

puts JSON.parse(response.body).dig("data", "is_active") # false
rust
// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f";

    // שלחו "url" ו/או "events" באותו אופן כדי לשנות את היעד או את סט האירועים.
    let payload: Value = reqwest::Client::new()
        .patch(format!("https://app.shipos.co.il/api/v2/webhooks/{uuid}"))
        .header("X-Client-Id", std::env::var("SHIPOS_CLIENT_ID")?)
        .header("X-Client-Secret", std::env::var("SHIPOS_CLIENT_SECRET")?)
        .header("Accept", "application/json")
        .json(&json!({
            "license_key": std::env::var("SHIPOS_LICENSE_KEY")?,
            "is_active": false,
        }))
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;

    println!("{}", payload["data"]["is_active"]); // false
    Ok(())
}

הסרת מינוי לחלוטין:

bash
curl --location --request DELETE 'https://app.shipos.co.il/api/v2/webhooks/{uuid}?license_key={license_key}' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json'
js
const uuid = '9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f';
const url = new URL(`https://app.shipos.co.il/api/v2/webhooks/${uuid}`);
url.searchParams.set('license_key', process.env.SHIPOS_LICENSE_KEY);

const res = await fetch(url, {
  method: 'DELETE',
  headers: {
    'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
    'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
    'Accept': 'application/json',
  },
});

const { data } = await res.json();
console.log(data.deleted); // true
php
<?php
// composer require guzzlehttp/guzzle

$uuid = '9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f';

$client = new \GuzzleHttp\Client([
    'base_uri' => 'https://app.shipos.co.il/api/v2/',
    'headers' => [
        'X-Client-Id' => getenv('SHIPOS_CLIENT_ID'),
        'X-Client-Secret' => getenv('SHIPOS_CLIENT_SECRET'),
        'Accept' => 'application/json',
    ],
]);

$response = $client->delete("webhooks/{$uuid}", [
    'query' => ['license_key' => getenv('SHIPOS_LICENSE_KEY')],
]);

$data = json_decode($response->getBody()->getContents(), true)['data'];

var_dump($data['deleted']); // true
php
<?php

use Illuminate\Support\Facades\Http;

$uuid = '9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f';

$deleted = Http::withHeaders([
    'X-Client-Id' => config('services.shipos.client_id'),
    'X-Client-Secret' => config('services.shipos.client_secret'),
])
    ->acceptJson()
    ->delete("https://app.shipos.co.il/api/v2/webhooks/{$uuid}", [
        'license_key' => config('services.shipos.license_key'),
    ])
    ->throw()
    ->json('data.deleted');

logger()->info('webhook deleted: '.var_export($deleted, true));
python
# pip install httpx
import os

import httpx

uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f"

response = httpx.delete(
    f"https://app.shipos.co.il/api/v2/webhooks/{uuid}",
    params={"license_key": os.environ["SHIPOS_LICENSE_KEY"]},
    headers={
        "X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
        "X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
        "Accept": "application/json",
    },
)
response.raise_for_status()

print(response.json()["data"]["deleted"])  # True
go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
)

func main() {
	uuid := "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f"
	query := url.Values{"license_key": {os.Getenv("SHIPOS_LICENSE_KEY")}}

	req, _ := http.NewRequest("DELETE",
		"https://app.shipos.co.il/api/v2/webhooks/"+uuid+"?"+query.Encode(), nil)
	req.Header.Set("X-Client-Id", os.Getenv("SHIPOS_CLIENT_ID"))
	req.Header.Set("X-Client-Secret", os.Getenv("SHIPOS_CLIENT_SECRET"))
	req.Header.Set("Accept", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var payload struct {
		Data struct {
			Deleted bool `json:"deleted"`
		} `json:"data"`
	}
	if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
		panic(err)
	}

	fmt.Println(payload.Data.Deleted) // true
}
java
// Java 17+ — java.net.http, ללא תלויות (פענוח עם Jackson/Gson)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class DeleteShipOsWebhook {
    public static void main(String[] args) throws Exception {
        String uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f";

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.shipos.co.il/api/v2/webhooks/" + uuid
                + "?license_key=" + System.getenv("SHIPOS_LICENSE_KEY")))
            .header("X-Client-Id", System.getenv("SHIPOS_CLIENT_ID"))
            .header("X-Client-Secret", System.getenv("SHIPOS_CLIENT_SECRET"))
            .header("Accept", "application/json")
            .DELETE()
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new RuntimeException("ShipOS error: " + response.body());
        }

        System.out.println(response.body()); // {"data":{"deleted":true}}
    }
}
csharp
// .NET 8+ — System.Net.Http.Json
using System.Net.Http.Json;
using System.Text.Json;

var uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f";
var licenseKey = Environment.GetEnvironmentVariable("SHIPOS_LICENSE_KEY");

using var http = new HttpClient
{
    BaseAddress = new Uri("https://app.shipos.co.il/api/v2/"),
};
http.DefaultRequestHeaders.Add("X-Client-Id",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_ID"));
http.DefaultRequestHeaders.Add("X-Client-Secret",
    Environment.GetEnvironmentVariable("SHIPOS_CLIENT_SECRET"));

var response = await http.DeleteAsync($"webhooks/{uuid}?license_key={licenseKey}");
response.EnsureSuccessStatusCode();

var payload = await response.Content.ReadFromJsonAsync<JsonDocument>()
    ?? throw new InvalidOperationException("Empty response");

Console.WriteLine(
    payload.RootElement.GetProperty("data").GetProperty("deleted").GetBoolean());
ruby
require "net/http"
require "json"

uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f"
uri = URI("https://app.shipos.co.il/api/v2/webhooks/#{uuid}")
uri.query = URI.encode_www_form(license_key: ENV.fetch("SHIPOS_LICENSE_KEY"))

request = Net::HTTP::Delete.new(uri)
request["X-Client-Id"] = ENV.fetch("SHIPOS_CLIENT_ID")
request["X-Client-Secret"] = ENV.fetch("SHIPOS_CLIENT_SECRET")
request["Accept"] = "application/json"

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

raise "ShipOS error: #{response.body}" unless response.is_a?(Net::HTTPSuccess)

puts JSON.parse(response.body).dig("data", "deleted") # true
rust
// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
use serde_json::Value;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let uuid = "9f2c1a7e-6b4d-4c2a-8e1f-7d3b2a1c0e9f";

    let payload: Value = reqwest::Client::new()
        .delete(format!("https://app.shipos.co.il/api/v2/webhooks/{uuid}"))
        .query(&[("license_key", std::env::var("SHIPOS_LICENSE_KEY")?)])
        .header("X-Client-Id", std::env::var("SHIPOS_CLIENT_ID")?)
        .header("X-Client-Secret", std::env::var("SHIPOS_CLIENT_SECRET")?)
        .header("Accept", "application/json")
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;

    println!("{}", payload["data"]["deleted"]); // true
    Ok(())
}

שניהם מחזירים את מעטפת { "data": ... } הסטנדרטית; {uuid} שגוי או זר מחזיר 404 not_found (ראו מדריך השגיאות).