Skip to content

SMS notifications

Set up customer-facing SMS: configure the provider, verify it with a test send, define per-shipping-method message templates, and audit what was actually sent. Endpoint details live in the Notifications reference.

Customer-scoped — no license_key

Unlike shipments and webhooks, the notification endpoints act on the merchant account behind your client credentials and cover all of its shipments across every license. They do not read a license_key.

Step 1 — Read the current settings

bash
curl --location 'https://app.shipos.co.il/api/v2/notifications/sms-settings' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json'
js
// Node.js 18+ / browsers — no dependencies
const response = await fetch('https://app.shipos.co.il/api/v2/notifications/sms-settings', {
  headers: {
    'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
    'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
    Accept: 'application/json',
  },
})

if (!response.ok) {
  const { error } = await response.json()
  throw new Error(`${error.code}: ${error.message}`)
}

const { data: settings } = await response.json()

console.log(settings.username, settings.has_token, settings.configured)
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',
    ],
]);

$settings = json_decode(
    $client->get('notifications/sms-settings')->getBody()->getContents(),
    true,
)['data'];

echo $settings['configured'] ? 'sending is set up' : 'not configured', PHP_EOL;
php
<?php

use Illuminate\Support\Facades\Http;

$settings = 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/notifications/sms-settings')
    ->throw()
    ->json('data');

logger()->info($settings['configured'] ? 'sending is set up' : 'not configured');
python
# pip install httpx
import os

import httpx

response = httpx.get(
    "https://app.shipos.co.il/api/v2/notifications/sms-settings",
    headers={
        "X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
        "X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
        "Accept": "application/json",
    },
)
response.raise_for_status()
settings = response.json()["data"]

print(settings["username"], settings["has_token"], settings["configured"])
go
package main

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

type smsSettingsResponse struct {
	Data struct {
		Username   string `json:"username"`
		HasToken   bool   `json:"has_token"`
		Configured bool   `json:"configured"`
	} `json:"data"`
}

func main() {
	req, _ := http.NewRequest("GET",
		"https://app.shipos.co.il/api/v2/notifications/sms-settings", 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 smsSettingsResponse
	if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
		panic(err)
	}

	fmt.Println(payload.Data.Username, payload.Data.HasToken, payload.Data.Configured)
}
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 ShipOsSmsSettings {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.shipos.co.il/api/v2/notifications/sms-settings"))
            .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":{"configured":true,...}}
    }
}
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 payload = await http.GetFromJsonAsync<JsonDocument>("notifications/sms-settings")
    ?? throw new InvalidOperationException("Empty response");
var settings = payload.RootElement.GetProperty("data");

Console.WriteLine(settings.GetProperty("username").GetString());
Console.WriteLine(settings.GetProperty("configured").GetBoolean());
ruby
require "net/http"
require "json"

uri = URI("https://app.shipos.co.il/api/v2/notifications/sms-settings")
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)

settings = JSON.parse(response.body).fetch("data")

puts "#{settings["username"]} has_token=#{settings["has_token"]} configured=#{settings["configured"]}"
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 payload: Value = reqwest::Client::new()
        .get("https://app.shipos.co.il/api/v2/notifications/sms-settings")
        .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?;

    let settings = &payload["data"];
    println!("{} configured={}", settings["username"], settings["configured"]);
    Ok(())
}

The settings are a singleton — this always returns 200, even before anything is configured:

json
{
  "data": {
    "id": 42,
    "username": "shipos_store",
    "sender_name": "MyStore",
    "has_token": true,
    "configured": true,
    "created_at": "2026-01-01T00:00:00.000000Z",
    "updated_at": "2026-07-20T12:30:00.000000Z"
  }
}

Note the two boolean flags: the provider token is write-only and never returned. has_token tells you a token is stored; configured is true only when both username and token are present — i.e. sending is actually set up.

Step 2 — Enable or update the provider

PUT the full settings. All three fields are required on every update (this is a full replace, not a patch):

bash
curl --location --request PUT 'https://app.shipos.co.il/api/v2/notifications/sms-settings' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "username": "shipos_store",
  "token": "provider-api-token-value",
  "sender_name": "MyStore"
}'
js
const res = await fetch('https://app.shipos.co.il/api/v2/notifications/sms-settings', {
  method: 'PUT',
  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({
    username: 'shipos_store',
    token: 'provider-api-token-value', // write-only: stored, never returned
    sender_name: 'MyStore',
  }),
});

const { data } = await res.json();
console.log(data.configured); // true when sending is set up
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->put('notifications/sms-settings', [
    'json' => [
        'username' => 'shipos_store',
        'token' => 'provider-api-token-value', // write-only: stored, never returned
        'sender_name' => 'MyStore',
    ],
]);

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

$configured = $settings['configured']; // true when sending is set up
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()
    ->put('https://app.shipos.co.il/api/v2/notifications/sms-settings', [
        'username' => 'shipos_store',
        'token' => 'provider-api-token-value', // write-only: stored, never returned
        'sender_name' => 'MyStore',
    ])
    ->throw();

$configured = $response->json('data.configured'); // true when sending is set up
python
# pip install httpx
import os

import httpx

response = httpx.put(
    "https://app.shipos.co.il/api/v2/notifications/sms-settings",
    headers={
        "X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
        "X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
        "Accept": "application/json",
    },
    json={
        "username": "shipos_store",
        "token": "provider-api-token-value",  # write-only: stored, never returned
        "sender_name": "MyStore",
    },
)
response.raise_for_status()

print(response.json()["data"]["configured"])  # True when sending is set up
go
package main

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

func main() {
	body, _ := json.Marshal(map[string]string{
		"username":    "shipos_store",
		"token":       "provider-api-token-value", // write-only: stored, never returned
		"sender_name": "MyStore",
	})

	req, _ := http.NewRequest("PUT",
		"https://app.shipos.co.il/api/v2/notifications/sms-settings", 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 {
			Configured bool `json:"configured"`
		} `json:"data"`
	}
	if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
		panic(err)
	}

	fmt.Println(payload.Data.Configured) // true when sending is set up
}
java
// Java 17+ — java.net.http, no dependencies
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class ShipOsUpdateSmsSettings {
    public static void main(String[] args) throws Exception {
        // token is write-only: stored, never returned
        String body = """
            {"username":"shipos_store","token":"provider-api-token-value","sender_name":"MyStore"}
            """;

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.shipos.co.il/api/v2/notifications/sms-settings"))
            .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")
            .PUT(HttpRequest.BodyPublishers.ofString(body))
            .build();

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

        System.out.println(response.body()); // {"data":{"configured":true,...}}
    }
}
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.PutAsJsonAsync("notifications/sms-settings", new
{
    username = "shipos_store",
    token = "provider-api-token-value", // write-only: stored, never returned
    sender_name = "MyStore",
});
response.EnsureSuccessStatusCode();

var payload = await response.Content.ReadFromJsonAsync<JsonDocument>();
Console.WriteLine(payload!.RootElement.GetProperty("data")
    .GetProperty("configured").GetBoolean()); // true when sending is set up
ruby
require "net/http"
require "json"

uri = URI("https://app.shipos.co.il/api/v2/notifications/sms-settings")
request = Net::HTTP::Put.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.dump(
  username: "shipos_store",
  token: "provider-api-token-value", # write-only: stored, never returned
  sender_name: "MyStore",
)

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

puts JSON.parse(response.body).dig("data", "configured") # true when sending is set up
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()
        .put("https://app.shipos.co.il/api/v2/notifications/sms-settings")
        .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!({
            "username": "shipos_store",
            // write-only: stored, never returned
            "token": "provider-api-token-value",
            "sender_name": "MyStore"
        }))
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;

    println!("{}", payload["data"]["configured"]); // true when sending is set up
    Ok(())
}
Body fieldRequiredNotes
usernameyesProvider account username. Max 255 chars.
tokenyesProvider API token. Write-only — the response only reflects it via has_token / configured.
sender_nameyesSender name shown to SMS recipients. Max 255 chars.

The response is the updated singleton (200), same shape as Step 1.

Step 3 — Send a test SMS

Verify the credentials actually work by sending a one-off message through the configured provider:

bash
curl --location 'https://app.shipos.co.il/api/v2/notifications/test-sms' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "phone": "0501234567",
  "message": "ShipOS test message"
}'
js
// Node.js 18+ / browsers — no dependencies
const res = await fetch('https://app.shipos.co.il/api/v2/notifications/test-sms', {
  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({
    phone: '0501234567',
    message: 'ShipOS test message',
  }),
})

// 424 also returns a `data` payload — branch on data.sent, not on `error`
const { data } = await res.json()
console.log(data.sent, data.provider_status, data.message)
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',
    ],
    'http_errors' => false, // a provider rejection is a 424 with a data payload
]);

$result = json_decode($client->post('notifications/test-sms', [
    'json' => [
        'phone' => '0501234567',
        'message' => 'ShipOS test message',
    ],
])->getBody()->getContents(), true)['data'];

echo $result['sent'] ? 'sent' : 'provider said no: '.$result['provider_status'], PHP_EOL;
php
<?php

use Illuminate\Support\Facades\Http;

$result = 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/notifications/test-sms', [
        'phone' => '0501234567',
        'message' => 'ShipOS test message',
    ])
    ->json('data'); // no ->throw(): 424 carries a data payload, not an error envelope

logger()->info($result['sent'] ? 'sent' : 'provider said no', $result);
python
# pip install httpx
import os

import httpx

response = httpx.post(
    "https://app.shipos.co.il/api/v2/notifications/test-sms",
    headers={
        "X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
        "X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
        "Accept": "application/json",
    },
    json={"phone": "0501234567", "message": "ShipOS test message"},
)

# 424 also returns a data payload — branch on data["sent"]
result = response.json()["data"]
print(result["sent"], result["provider_status"], result["message"])
go
package main

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

func main() {
	body, _ := json.Marshal(map[string]string{
		"phone":   "0501234567",
		"message": "ShipOS test message",
	})

	req, _ := http.NewRequest("POST",
		"https://app.shipos.co.il/api/v2/notifications/test-sms", 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()

	// 200 and 424 both carry a data payload — branch on Sent
	var payload struct {
		Data struct {
			Sent           bool   `json:"sent"`
			ProviderStatus string `json:"provider_status"`
			Message        string `json:"message"`
		} `json:"data"`
	}
	if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
		panic(err)
	}

	fmt.Println(payload.Data.Sent, payload.Data.ProviderStatus, payload.Data.Message)
}
java
// Java 17+ — java.net.http, no dependencies
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class ShipOsTestSms {
    public static void main(String[] args) throws Exception {
        String body = """
            {"phone":"0501234567","message":"ShipOS test message"}
            """;

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.shipos.co.il/api/v2/notifications/test-sms"))
            .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());

        // 424 = provider rejection, still a data payload — inspect data.sent
        System.out.println(response.statusCode() + " " + 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("notifications/test-sms", new
{
    phone = "0501234567",
    message = "ShipOS test message",
});

// 424 also returns a data payload — branch on data.sent, not on an error key
var payload = await response.Content.ReadFromJsonAsync<JsonDocument>();
var result = payload!.RootElement.GetProperty("data");

Console.WriteLine($"{result.GetProperty("sent").GetBoolean()} " +
    $"{result.GetProperty("provider_status").GetString()}");
ruby
require "net/http"
require "json"

uri = URI("https://app.shipos.co.il/api/v2/notifications/test-sms")
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.dump(phone: "0501234567", message: "ShipOS test message")

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

# 424 also returns a data payload — branch on data["sent"]
result = JSON.parse(response.body).fetch("data")

puts "#{result["sent"]} #{result["provider_status"]} #{result["message"]}"
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>> {
    // no error_for_status(): a 424 provider rejection still carries a data payload
    let payload: Value = reqwest::Client::new()
        .post("https://app.shipos.co.il/api/v2/notifications/test-sms")
        .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!({ "phone": "0501234567", "message": "ShipOS test message" }))
        .send()
        .await?
        .json()
        .await?;

    let result = &payload["data"];
    println!("{} {}", result["sent"], result["provider_status"]);
    Ok(())
}

phone (max 20 chars) and message (max 1000 chars) are both required.

On success you get 200:

json
{
  "data": {
    "sent": true,
    "provider_status": "OK",
    "message": "..."
  }
}

A provider rejection is a 424 with a data payload

When the SMS provider refuses the send (bad token, blocked sender name, invalid destination), the response status is 424 — but the body is still a data payload (sent: false plus the provider's provider_status / message), not the { "error": ... } envelope. This deliberately distinguishes "we tried, the provider said no" from a request error on your side. Branch on the HTTP status or on data.sent, not on the presence of an error key. See the Errors guide for the normal envelope.

Step 4 — Manage templates

Templates define the message sent per shipping method. List what exists:

bash
curl --location 'https://app.shipos.co.il/api/v2/notifications/sms-templates' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json'
js
// Node.js 18+ / browsers — no dependencies
const response = await fetch('https://app.shipos.co.il/api/v2/notifications/sms-templates', {
  headers: {
    'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
    'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
    Accept: 'application/json',
  },
})

if (!response.ok) {
  const { error } = await response.json()
  throw new Error(`${error.code}: ${error.message}`)
}

const { data: templates } = await response.json()

for (const template of templates) {
  console.log(template.id, template.shipping_method, template.active)
}
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',
    ],
]);

$templates = json_decode(
    $client->get('notifications/sms-templates')->getBody()->getContents(),
    true,
)['data'];

foreach ($templates as $template) {
    echo $template['id'], ' ', $template['shipping_method'], PHP_EOL;
}
php
<?php

use Illuminate\Support\Facades\Http;

$templates = 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/notifications/sms-templates')
    ->throw()
    ->json('data');

foreach ($templates as $template) {
    logger()->info($template['shipping_method'].' → '.$template['message']);
}
python
# pip install httpx
import os

import httpx

response = httpx.get(
    "https://app.shipos.co.il/api/v2/notifications/sms-templates",
    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 template in response.json()["data"]:
    print(template["id"], template["shipping_method"], template["active"])
go
package main

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

type smsTemplatesResponse struct {
	Data []struct {
		ID             int    `json:"id"`
		ShippingMethod string `json:"shipping_method"`
		Active         bool   `json:"active"`
	} `json:"data"`
}

func main() {
	req, _ := http.NewRequest("GET",
		"https://app.shipos.co.il/api/v2/notifications/sms-templates", 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 smsTemplatesResponse
	if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
		panic(err)
	}

	for _, template := range payload.Data {
		fmt.Println(template.ID, template.ShippingMethod, template.Active)
	}
}
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 ShipOsListSmsTemplates {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.shipos.co.il/api/v2/notifications/sms-templates"))
            .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":[{"id":7,...}]}
    }
}
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 payload = await http.GetFromJsonAsync<JsonDocument>("notifications/sms-templates")
    ?? throw new InvalidOperationException("Empty response");

foreach (var template in payload.RootElement.GetProperty("data").EnumerateArray())
{
    Console.WriteLine($"{template.GetProperty("id").GetInt32()} " +
        $"{template.GetProperty("shipping_method").GetString()}");
}
ruby
require "net/http"
require "json"

uri = URI("https://app.shipos.co.il/api/v2/notifications/sms-templates")
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 |template|
  puts "#{template["id"]} #{template["shipping_method"]} active=#{template["active"]}"
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 payload: Value = reqwest::Client::new()
        .get("https://app.shipos.co.il/api/v2/notifications/sms-templates")
        .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?;

    if let Some(templates) = payload["data"].as_array() {
        for template in templates {
            println!("{} {}", template["id"], template["shipping_method"]);
        }
    }
    Ok(())
}

Create (or update — the API upserts per shipping method) a template. Body: shipping_method (string, max 64) and message (string, max 1000):

bash
curl --location 'https://app.shipos.co.il/api/v2/notifications/sms-templates' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "shipping_method": "hfd",
  "message": "Your order is on its way! Track it here: {tracking_url}"
}'
js
const res = await fetch('https://app.shipos.co.il/api/v2/notifications/sms-templates', {
  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({
    shipping_method: 'hfd',
    message: 'Your order is on its way! Track it here: {tracking_url}',
  }),
});

const { data } = await res.json();
const templateId = data.id; // integer id — used in toggle/delete URLs
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('notifications/sms-templates', [
    'json' => [
        'shipping_method' => 'hfd',
        'message' => 'Your order is on its way! Track it here: {tracking_url}',
    ],
]);

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

$templateId = $template['id']; // integer id — used in toggle/delete URLs
php
<?php

use Illuminate\Support\Facades\Http;

$template = 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/notifications/sms-templates', [
        'shipping_method' => 'hfd',
        'message' => 'Your order is on its way! Track it here: {tracking_url}',
    ])
    ->throw()
    ->json('data');

$templateId = $template['id']; // integer id — used in toggle/delete URLs
python
# pip install httpx
import os

import httpx

response = httpx.post(
    "https://app.shipos.co.il/api/v2/notifications/sms-templates",
    headers={
        "X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
        "X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
        "Accept": "application/json",
    },
    json={
        "shipping_method": "hfd",
        "message": "Your order is on its way! Track it here: {tracking_url}",
    },
)
response.raise_for_status()

template_id = response.json()["data"]["id"]  # integer id — used in toggle/delete URLs
go
package main

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

func main() {
	body, _ := json.Marshal(map[string]string{
		"shipping_method": "hfd",
		"message":         "Your order is on its way! Track it here: {tracking_url}",
	})

	req, _ := http.NewRequest("POST",
		"https://app.shipos.co.il/api/v2/notifications/sms-templates", 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 int `json:"id"`
		} `json:"data"`
	}
	if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
		panic(err)
	}

	fmt.Println(payload.Data.ID) // integer id — used in toggle/delete URLs
}
java
// Java 17+ — java.net.http, no dependencies
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class ShipOsUpsertSmsTemplate {
    public static void main(String[] args) throws Exception {
        // {tracking_url} is a ShipOS placeholder — keep it literal, do not format it
        String body = """
            {"shipping_method":"hfd","message":"Your order is on its way! Track it here: {tracking_url}"}
            """;

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.shipos.co.il/api/v2/notifications/sms-templates"))
            .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.id is the integer id — used in toggle/delete URLs
        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"));

// Not an interpolated string: {tracking_url} must reach the API literally
var response = await http.PostAsJsonAsync("notifications/sms-templates", new
{
    shipping_method = "hfd",
    message = "Your order is on its way! Track it here: {tracking_url}",
});
response.EnsureSuccessStatusCode();

var payload = await response.Content.ReadFromJsonAsync<JsonDocument>();
var templateId = payload!.RootElement.GetProperty("data")
    .GetProperty("id").GetInt32(); // integer id — used in toggle/delete URLs
ruby
require "net/http"
require "json"

uri = URI("https://app.shipos.co.il/api/v2/notifications/sms-templates")
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.dump(
  shipping_method: "hfd",
  # single-quoted: {tracking_url} is a ShipOS placeholder, not Ruby interpolation
  message: 'Your order is on its way! Track it here: {tracking_url}',
)

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

template_id = JSON.parse(response.body).dig("data", "id") # used in toggle/delete URLs
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/notifications/sms-templates")
        .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!({
            "shipping_method": "hfd",
            // {tracking_url} is a ShipOS placeholder — sent literally
            "message": "Your order is on its way! Track it here: {tracking_url}"
        }))
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;

    // integer id — used in toggle/delete URLs
    println!("{}", payload["data"]["id"]);
    Ok(())
}

The 201 response returns the upserted template:

json
{
  "data": {
    "id": 7,
    "shipping_method": "hfd",
    "delivery_type": null,
    "message": "Your order is on its way! Track it here: {tracking_url}",
    "active": true,
    "created_at": "2026-07-29T10:00:00.000000Z",
    "updated_at": "2026-07-29T10:00:00.000000Z"
  }
}

Toggle a template's active state or delete it — {template} is the integer id from the resource, not a UUID:

bash
# Toggle on/off
curl --location --request PUT 'https://app.shipos.co.il/api/v2/notifications/sms-templates/7/toggle' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json'

# Delete
curl --location --request DELETE 'https://app.shipos.co.il/api/v2/notifications/sms-templates/7' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json'
js
const base = 'https://app.shipos.co.il/api/v2/notifications/sms-templates'
const headers = {
  'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
  'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
  Accept: 'application/json',
}

// Toggle on/off
const toggled = await fetch(`${base}/7/toggle`, { method: 'PUT', headers })
console.log((await toggled.json()).data.active)

// Delete
const deleted = await fetch(`${base}/7`, { method: 'DELETE', headers })
console.log((await deleted.json()).data.deleted) // true
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',
    ],
]);

// Toggle on/off
$template = json_decode(
    $client->put('notifications/sms-templates/7/toggle')->getBody()->getContents(),
    true,
)['data'];

// Delete
$client->delete('notifications/sms-templates/7');
php
<?php

use Illuminate\Support\Facades\Http;

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

// Toggle on/off
$template = $shipos
    ->put('https://app.shipos.co.il/api/v2/notifications/sms-templates/7/toggle')
    ->throw()
    ->json('data');

// Delete
$shipos->delete('https://app.shipos.co.il/api/v2/notifications/sms-templates/7')->throw();
python
# pip install httpx
import os

import httpx

base = "https://app.shipos.co.il/api/v2/notifications/sms-templates"
headers = {
    "X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
    "X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
    "Accept": "application/json",
}

# Toggle on/off
toggled = httpx.put(f"{base}/7/toggle", headers=headers)
toggled.raise_for_status()
print(toggled.json()["data"]["active"])

# Delete
deleted = httpx.delete(f"{base}/7", headers=headers)
deleted.raise_for_status()
print(deleted.json()["data"]["deleted"])  # True
go
package main

import (
	"fmt"
	"net/http"
	"os"
)

const base = "https://app.shipos.co.il/api/v2/notifications/sms-templates"

func call(method, url string) *http.Response {
	req, _ := http.NewRequest(method, url, 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)
	}

	return res
}

func main() {
	toggled := call("PUT", base+"/7/toggle") // Toggle on/off
	defer toggled.Body.Close()

	deleted := call("DELETE", base+"/7") // Delete
	defer deleted.Body.Close()

	fmt.Println(toggled.StatusCode, deleted.StatusCode)
}
java
// Java 17+ — java.net.http, no dependencies
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class ShipOsManageSmsTemplate {
    static final String BASE =
        "https://app.shipos.co.il/api/v2/notifications/sms-templates";

    static HttpResponse<String> send(String method, String path) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(BASE + path))
            .header("X-Client-Id", System.getenv("SHIPOS_CLIENT_ID"))
            .header("X-Client-Secret", System.getenv("SHIPOS_CLIENT_SECRET"))
            .header("Accept", "application/json")
            .method(method, HttpRequest.BodyPublishers.noBody())
            .build();

        return HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
    }

    public static void main(String[] args) throws Exception {
        System.out.println(send("PUT", "/7/toggle").body()); // Toggle on/off
        System.out.println(send("DELETE", "/7").body());     // Delete
    }
}
csharp
// .NET 8+
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"));

// Toggle on/off
var toggled = await http.PutAsync("notifications/sms-templates/7/toggle", null);
toggled.EnsureSuccessStatusCode();
Console.WriteLine(await toggled.Content.ReadAsStringAsync());

// Delete
var deleted = await http.DeleteAsync("notifications/sms-templates/7");
deleted.EnsureSuccessStatusCode();
Console.WriteLine(await deleted.Content.ReadAsStringAsync()); // {"data":{"deleted":true}}
ruby
require "net/http"
require "json"

BASE = URI("https://app.shipos.co.il/api/v2/notifications/sms-templates/")

def call(request)
  request["X-Client-Id"] = ENV.fetch("SHIPOS_CLIENT_ID")
  request["X-Client-Secret"] = ENV.fetch("SHIPOS_CLIENT_SECRET")
  request["Accept"] = "application/json"

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

# Toggle on/off
toggled = call(Net::HTTP::Put.new(BASE + "7/toggle"))
puts JSON.parse(toggled.body).dig("data", "active")

# Delete
deleted = call(Net::HTTP::Delete.new(BASE + "7"))
puts JSON.parse(deleted.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;

const BASE: &str = "https://app.shipos.co.il/api/v2/notifications/sms-templates";

fn auth(req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
    req.header("X-Client-Id", std::env::var("SHIPOS_CLIENT_ID").unwrap())
        .header("X-Client-Secret", std::env::var("SHIPOS_CLIENT_SECRET").unwrap())
        .header("Accept", "application/json")
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();

    // Toggle on/off
    let toggled: Value = auth(client.put(format!("{BASE}/7/toggle")))
        .send().await?.error_for_status()?.json().await?;
    println!("{}", toggled["data"]["active"]);

    // Delete
    let deleted: Value = auth(client.delete(format!("{BASE}/7")))
        .send().await?.error_for_status()?.json().await?;
    println!("{}", deleted["data"]["deleted"]); // true
    Ok(())
}

Toggle returns the updated template; delete returns { "data": { "deleted": true } }. A template id that doesn't belong to your account returns 404 not_found.

Step 5 — Audit with the SMS log

Every send attempt is logged. Query the log with optional filters:

bash
curl --location 'https://app.shipos.co.il/api/v2/notifications/sms-logs?filter[success]=false&per_page=50' \
--header 'X-Client-Id: {client_id}' \
--header 'X-Client-Secret: {client_secret}' \
--header 'Accept: application/json'
js
const url = new URL('https://app.shipos.co.il/api/v2/notifications/sms-logs')
url.searchParams.set('filter[success]', 'false')
url.searchParams.set('per_page', '50')

const response = 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: logs } = await response.json()

for (const log of logs) {
  console.log(log.phone, log.state, log.provider.message)
}
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->get('notifications/sms-logs', [
    'query' => ['filter[success]' => 'false', 'per_page' => 50],
]);

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

foreach ($logs as $log) {
    echo $log['phone'], ' ', $log['state'], ' ', $log['provider']['message'], PHP_EOL;
}
php
<?php

use Illuminate\Support\Facades\Http;

$logs = 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/notifications/sms-logs', [
        'filter[success]' => 'false',
        'per_page' => 50,
    ])
    ->throw()
    ->json('data');

foreach ($logs as $log) {
    logger()->warning($log['phone'].' → '.data_get($log, 'provider.message'));
}
python
# pip install httpx
import os

import httpx

response = httpx.get(
    "https://app.shipos.co.il/api/v2/notifications/sms-logs",
    headers={
        "X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
        "X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
        "Accept": "application/json",
    },
    params={"filter[success]": "false", "per_page": 50},
)
response.raise_for_status()

for log in response.json()["data"]:
    print(log["phone"], log["state"], log["provider"]["message"])
go
package main

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

func main() {
	query := url.Values{}
	query.Set("filter[success]", "false")
	query.Set("per_page", "50")

	req, _ := http.NewRequest("GET",
		"https://app.shipos.co.il/api/v2/notifications/sms-logs?"+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 {
			Phone   string `json:"phone"`
			State   string `json:"state"`
			Success bool   `json:"success"`
		} `json:"data"`
	}
	if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
		panic(err)
	}

	for _, log := range payload.Data {
		fmt.Println(log.Phone, log.State, log.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 ShipOsSmsLogs {
    public static void main(String[] args) throws Exception {
        // Brackets must be percent-encoded: filter[success] → filter%5Bsuccess%5D
        String url = "https://app.shipos.co.il/api/v2/notifications/sms-logs"
            + "?filter%5Bsuccess%5D=false&per_page=50";

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .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());

        System.out.println(response.body()); // {"data":[{"id":1201,...}]}
    }
}
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"));

// Brackets percent-encoded: filter[success] → filter%5Bsuccess%5D
var payload = await http.GetFromJsonAsync<JsonDocument>(
    "notifications/sms-logs?filter%5Bsuccess%5D=false&per_page=50")
    ?? throw new InvalidOperationException("Empty response");

foreach (var log in payload.RootElement.GetProperty("data").EnumerateArray())
{
    Console.WriteLine($"{log.GetProperty("phone").GetString()} " +
        $"{log.GetProperty("state").GetString()}");
}
ruby
require "net/http"
require "json"

uri = URI("https://app.shipos.co.il/api/v2/notifications/sms-logs")
uri.query = URI.encode_www_form("filter[success]" => "false", "per_page" => 50)

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

JSON.parse(response.body).fetch("data").each do |log|
  puts "#{log["phone"]} #{log["state"]} #{log.dig("provider", "message")}"
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 payload: Value = reqwest::Client::new()
        .get("https://app.shipos.co.il/api/v2/notifications/sms-logs")
        .query(&[("filter[success]", "false"), ("per_page", "50")])
        .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?;

    if let Some(logs) = payload["data"].as_array() {
        for log in logs {
            println!("{} {}", log["phone"], log["provider"]["message"]);
        }
    }
    Ok(())
}
Query paramDescription
filter[shipping_id]Only logs for one shipment.
filter[success]true / false — only successful or failed sends.
per_pagePage size, default 25, max 100.

Each log entry:

json
{
  "data": [
    {
      "id": 1201,
      "shipment_id": "b7e4c9d2-1a3f-4e5b-9c8d-2f1e0a9b8c7d",
      "shipping_method": "hfd",
      "phone": "0501234567",
      "message": "Your order is on its way! ...",
      "success": true,
      "state": "sent",
      "provider": { "status": "OK", "message": "..." },
      "created_at": "2026-07-29T10:05:00.000000Z",
      "updated_at": "2026-07-29T10:05:00.000000Z"
    }
  ]
}

filter[success]=false plus the provider.status / provider.message fields is the fastest way to answer "why didn't the customer get their SMS?".

See also