קבלת 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 אם בבעלות החשבון שלכם יותר מרישיון פעיל אחד (ראו מדריך רישיונות).
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"]
}'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
// 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
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_… — שמרו אותו עכשיו, מוצג פעם אחת בלבד# 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_… — שמרו אותו עכשיו, מוצג פעם אחת בלבד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 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());
}
}// .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());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_… — שמרו אותו עכשיו, מוצג פעם אחת בלבד// [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 לחתימה:
{
"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} הוא גוף הבקשה הגולמי בדיוק כפי שהתקבל. הגוף עצמו נראה כך:
{
"event": "shipment.created",
"created_at": 1753783200,
"data": {
"id": "b7e4c9d2-1a3f-4e5b-9c8d-2f1e0a9b8c7d",
"tracking_code": "66747921",
"service_type": 1
}
}אמתו לפני שאתם סומכים על משהו:
- פצלו את ה-header ל-
tו-v1. - חשבו
HMAC-SHA256(secret, t + "." + rawBody). - השוו מול
v1בהשוואה בטוחה מבחינת תזמון (timing-safe). - אופציונלית, דחו ערכי
tישנים כדי להגביל חלונות replay (הסבילות היא בחירה שלכם; 5 דקות היא ברירת מחדל נפוצה — שימו לב שניסיונות חוזרים לגיטימיים משתמשים שוב בחותמת הזמן המקורית, לכן שמרו על חלון רחב לפחות כמו לוח הניסיונות החוזרים, בערך 2–3 דקות, או בצעו דה-דופליקציה במקום לדחות).
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
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
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]);
}
}# 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 "", 200package 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 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")));
}
}// .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));
}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// [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 שלכם:
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'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
// 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
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));# 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"]) # Truepackage 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 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}}
}
}// .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());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// [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 והגוף הזה:
{
"event": "ping",
"created_at": 1753783200,
"data": { "message": "This is a test event from ShipOS." }
}ping הוא שם אירוע לבדיקות בלבד — הוא אינו חלק מרשימת האירועים הזמינים למינוי, ולכן ה-handler שלכם צריך לקבל אותו (או לפחות להחזיר עבורו 2xx) אף על פי שמעולם לא נרשמתם אליו.
שלב 4 — דיבוג deliveries
כל ניסיון delivery מתועד. בחנו אותם עם:
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'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
// 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
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']);
}# 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"])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 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
}
}// .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()}");
}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// [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:
{
"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). |
success | true כאשר ה-endpoint שלכם החזיר 2xx. |
error | HTTP {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). שמרו מפתחות שעובדו ודלגו על חזרות.
// דוגמה לדה-דופליקציה באמצעות 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 לחתימה לעולם אינו משתנה:
# השהיית מינוי בלי למחוק אותו
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"]
}'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
// 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
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));# 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"]) # Falsepackage 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 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}}
}
}// .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());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// [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(())
}הסרת מינוי לחלוטין:
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'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
// 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
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));# 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"]) # Truepackage 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 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}}
}
}// .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());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// [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 (ראו מדריך השגיאות).