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:
| Event | Fires when |
|---|---|
shipment.created | A shipment is created. |
shipment.status_changed | A shipment's status changes. |
shipment.delivered | A shipment is delivered. |
shipment.cancelled | A shipment is cancelled. |
Webhooks are license-scoped, so include license_key if your account owns more than one active license (see Licenses guide).
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_… — store it now, shown only once<?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
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# 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 oncepackage 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 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());
}
}// .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());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// [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:
{
"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 formatt={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:
{
"event": "shipment.created",
"created_at": 1753783200,
"data": {
"id": "b7e4c9d2-1a3f-4e5b-9c8d-2f1e0a9b8c7d",
"tracking_code": "66747921",
"service_type": 1
}
}Verify before trusting anything:
- Split the header into
tandv1. - Compute
HMAC-SHA256(secret, t + "." + rawBody). - Compare against
v1with a timing-safe comparison. - Optionally reject stale
tvalues 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).
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
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
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]);
}
}# 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 "", 200package 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 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")));
}
}// .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));
}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// [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:
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 — enqueued, not yet accepted by the receiver<?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, 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}}
}
}// .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(())
}The API responds { "data": { "queued": true } } and your endpoint shortly receives a signed delivery with X-ShipOS-Event: ping and this body:
{
"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:
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, 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
}
}// .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(())
}The response is paginated (per_page defaults to 25, max 100). Each delivery row:
{
"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"
}
]
}| Field | Meaning |
|---|---|
event | The event that was delivered. |
attempt | Which retry attempt this row records (1 = first try). |
status_code | HTTP status your endpoint returned, or null on a transport failure (timeout, DNS, TLS). |
success | true when your endpoint returned a 2xx. |
error | HTTP {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_attimestamp, 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.
// 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:
# 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"]
}'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
// 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
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));# 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"]) # Falsepackage 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 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}}
}
}// .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());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// [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:
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, 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}}
}
}// .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(())
}Both return the standard { "data": ... } envelope; a wrong or foreign {uuid} returns 404 not_found (see the Errors guide).