Skip to content

Receive webhooks

Instead of polling GET /shipments/{shipment}/status, subscribe a URL and let ShipOS push signed JSON events to you as they happen. This tutorial takes you from subscription to a production-ready, replay-safe receiver. Endpoint-by-endpoint details live in the Webhooks reference.

Step 1 — Subscribe

Create a subscription with POST /webhooks, giving the HTTPS URL to deliver to and the events you want. The subscribable events are:

EventFires when
shipment.createdA shipment is created.
shipment.status_changedA shipment's status changes.
shipment.deliveredA shipment is delivered.
shipment.cancelledA shipment is cancelled.

Webhooks are license-scoped, so include license_key if your account owns more than one active license (see Licenses guide).

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_… — store it now, shown only once
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_… — store it now, shown only once
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_… — store it now, shown only once
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_… — store it now, shown only once
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_… — store it now, shown only once
	fmt.Println(payload.Data.ID, payload.Data.Secret)
}
java
// Java 17+ — java.net.http, no dependencies (parse with 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 is whsec_… — store it now, shown only once
        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_… — store it now, shown only once
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_… — store it now, shown only once
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_… — store it now, shown only once
    println!("{}", payload["data"]["secret"]);
    Ok(())
}

The 201 response includes the subscription and — only on this create response — the signing 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"
  }
}

Store the secret now

The whsec_… secret is write-only. It appears in the create response and never again — not in GET /webhooks, not in updates. If you lose it, delete the subscription and create a new one.

Step 2 — Verify signatures

Every delivery is an HTTP POST with two headers:

  • X-ShipOS-Event — the event name (e.g. shipment.created).
  • X-ShipOS-Signature — in the format t={timestamp},v1={hmac}.

The {hmac} is a hex-encoded HMAC-SHA256 over the string "{timestamp}.{body}", keyed with your full whsec_… secret, where {body} is the raw request body exactly as received. The body itself looks like:

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

Verify before trusting anything:

  1. Split the header into t and v1.
  2. Compute HMAC-SHA256(secret, t + "." + rawBody).
  3. Compare against v1 with a timing-safe comparison.
  4. Optionally reject stale t values to limit replay windows (the tolerance is your choice; 5 minutes is a common default — note that legitimate retries reuse the original timestamp, so keep the window at least as wide as the retry schedule, about 2–3 minutes, or dedupe instead of rejecting).
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 format: 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;

  // Optional replay protection.
  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); // timing-safe
}

// IMPORTANT: verify against the RAW body, not a re-serialized JSON object.
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);
  // ... handle event, then respond 2xx
  res.sendStatus(200);
});
php
<?php

function verifyShipOsSignature(string $header, string $rawBody, string $secret, int $toleranceSeconds = 300): bool
{
    // Header format: 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;
    }

    // Optional replay protection.
    if (abs(time() - (int) $parts['t']) > $toleranceSeconds) {
        return false;
    }

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

    return hash_equals($expected, $parts['v1']); // timing-safe
}

// Usage (plain PHP endpoint) — php://input is the RAW body, never a re-encoded array.
$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);
// ... handle $event, then respond 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() is the RAW body — never hash a re-encoded json() array.
        $rawBody = $request->getContent();

        // Header format: t={timestamp},v1={hmac}
        parse_str(str_replace(',', '&', (string) $request->header('X-ShipOS-Signature')), $parts);

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

        // Optional replay protection.
        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); // timing-safe

        // ... dispatch a job with $request->json()->all(), then respond 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 format: 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

    # Optional replay protection.
    if abs(time.time() - int(parts["t"])) > tolerance:
        return False

    signed = parts["t"].encode() + b"." + raw_body  # RAW bytes, never a re-dumped dict
    expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()

    return hmac.compare_digest(expected, parts["v1"])  # timing-safe


@app.post("/hooks/shipos")
def shipos_hook():
    # request.get_data() returns the RAW body exactly as received.
    if not verify_shipos_signature(
        request.headers.get("X-ShipOS-Signature", ""), request.get_data()
    ):
        return "invalid signature", 400

    event = request.get_json()
    # ... handle event, then respond 2xx
    return "", 200
go
package main

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

// rawBody must be the RAW request bytes — never a re-marshalled struct.
func verifyShipOsSignature(header string, rawBody []byte, secret string) bool {
	// Header format: 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 // bad timestamp, or optional replay window exceeded
	}

	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) // timing-safe
}

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
		}

		// ... handle the event, then respond 2xx
		w.WriteHeader(http.StatusOK)
	})

	http.ListenAndServe(":8080", nil)
}
java
// Java 17+ — javax.crypto, no dependencies
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 must be the RAW request bytes — never a re-serialised object. */
    public static boolean verify(String header, byte[] rawBody) throws Exception {
        // Header format: 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; // optional replay protection
        }

        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);

        // timing-safe comparison
        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 must be the RAW request bytes — never a re-serialised object.
static bool VerifyShipOsSignature(string header, byte[] rawBody, int toleranceSeconds = 300)
{
    // Header format: 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;
    }

    // Optional replay protection.
    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 format: 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"]

  # Optional replay protection.
  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"]) # timing-safe
end

post "/hooks/shipos" do
  raw_body = request.body.read # RAW body — never a re-generated JSON string
  unless verify_shipos_signature(request.env["HTTP_X_SHIPOS_SIGNATURE"], raw_body)
    halt 400, "invalid signature"
  end

  event = JSON.parse(raw_body)
  # ... handle event, then respond 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` must be the RAW request bytes — never a re-serialised struct.
fn verify_shipos_signature(header: &str, raw_body: &[u8], secret: &str) -> bool {
    // Header format: 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; // optional replay protection
    }

    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() // timing-safe
}

Always sign over the raw bytes

Compute the HMAC over the request body exactly as received. Parsing the JSON and re-serializing it will change key order, unicode escaping, or whitespace and the signature will not match.

Step 3 — Test with a ping

Queue a test delivery to your 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 — enqueued, not yet accepted by the receiver
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, no dependencies (parse with 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(())
}

The API responds { "data": { "queued": true } } and your endpoint shortly receives a signed delivery with X-ShipOS-Event: ping and this body:

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

ping is a test-only event name — it is not part of the subscribable event list, so your handler should accept it (or at least return 2xx for it) even though you never subscribed to it.

Step 4 — Debug deliveries

Every delivery attempt is recorded. Inspect them with:

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, no dependencies (parse with 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":[...]} — map with 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(())
}

The response is paginated (per_page defaults to 25, max 100). Each delivery row:

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"
    }
  ]
}
FieldMeaning
eventThe event that was delivered.
attemptWhich retry attempt this row records (1 = first try).
status_codeHTTP status your endpoint returned, or null on a transport failure (timeout, DNS, TLS).
successtrue when your endpoint returned a 2xx.
errorHTTP {status} for non-2xx responses, or the transport error message; null on success.

Step 5 — Handle retries idempotently

A delivery is considered failed on any non-2xx response or transport error (the request times out after 15 seconds). Failed deliveries are retried up to 5 attempts total, with a 30-second backoff between attempts. Two consequences for your handler:

  • Respond 2xx fast. Do the minimum (verify signature, enqueue for processing), then return 200. If your heavy processing makes the request exceed 15 seconds, ShipOS records a failure and re-delivers.
  • Expect duplicates. A retry re-sends the identical body and signature (same created_at timestamp, same payload). And even a 2xx that ShipOS never sees (e.g. dropped connection) triggers a re-delivery. Dedupe before acting: a good key is a hash of the raw body, or the tuple (event, data.id, created_at). Store processed keys and skip repeats.
js
// Example dedupe using the raw body hash
const deliveryKey = crypto.createHash('sha256').update(rawBody).digest('hex');
if (await alreadyProcessed(deliveryKey)) return res.sendStatus(200);
await markProcessed(deliveryKey);

TIP

Events may also arrive out of order (queued jobs, retries). Treat shipment.status_changed payloads as a trigger to re-read the shipment via GET /shipments/{shipment} rather than as the source of truth for ordering.

Step 6 — Lifecycle: update and remove

Change the URL, the event set, or pause deliveries with a partial PATCH — every field is optional, and the signing secret never changes:

bash
# Pause a subscription without deleting it
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
}'

# Change the URL and the event set
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',
  },
  // Send `url` and/or `events` the same way to change the destination or event set.
  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',
    ],
]);

// Send 'url' and/or 'events' the same way to change the destination or event set.
$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';

// Send 'url' and/or 'events' the same way to change the destination or event set.
$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"

# Send "url" and/or "events" the same way to change the destination or event set.
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"

	// Send "url" and/or "events" the same way to change the destination or event set.
	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, no dependencies (parse with 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";

        // Send "url" and/or "events" the same way to change destination or event set.
        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"));

// Send url / events the same way to change the destination or event set.
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"
# Send :url and/or :events the same way to change the destination or event set.
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";

    // Send "url" and/or "events" the same way to change destination or event set.
    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(())
}

Remove a subscription entirely:

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, no dependencies (parse with 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(())
}

Both return the standard { "data": ... } envelope; a wrong or foreign {uuid} returns 404 not_found (see the Errors guide).