Appearance
API Quickstart
Base URL:
https://your-domain.com
This guide takes you from zero to a working integration in five steps, in the language of your choice:
- Install an HTTP client
- Log in and get a token
- Call a protected endpoint
- Keep the session alive — refresh the token before it expires, re-login when it has
- Create a subscriber and list subscribers
At the end you'll find a complete, reusable client that bundles all of this.
Pick your language once
Every code block on this site has cURL · PHP · JavaScript · Python · Go · Rust tabs. Select a tab anywhere and every other example switches to the same language — the choice is remembered on your next visit.
How authentication works
| Login | POST /api/v1/users/login with email (or username) + password |
| Token | Send Authorization: Bearer <token> on every protected request |
| Lifetime | 30 days by default (server-configurable) — the login response tells you exactly when in data.expires_at |
| Refresh | POST /api/v1/users/refresh-token returns a new token and revokes the old one |
| Expired? | Requests return 401 with {"message": "Unauthenticated."} — an expired token can't be refreshed, log in again |
Tokens can die before expires_at
A token is revoked immediately — regardless of its lifetime — when the user is disabled, when the user's password is changed (by anyone, including an admin), or when logout / logout-all is called. Refreshing also rotates the token: the old one stops working the moment the new one is issued. So never assume a stored token is valid just because expires_at is in the future — always handle 401 by logging in again.
The robust pattern used by the client below:
- Before each request, if less than a day of lifetime is left, refresh the token.
- If a request still comes back
401, log in again and retry the request once.
Multi-tenancy
Most endpoints require isp_id and branch_id. Use the values returned in data.user from the login response unless you're an admin working across several ISPs.
1. Install an HTTP client
bash
composer require guzzlehttp/guzzlebash
# Node.js 18+ ships with fetch — nothing to install.
node --versionbash
pip install requestsbash
# Uses the standard library only.
go mod init zalultra-examplebash
cargo add reqwest --features json
cargo add tokio --features full
cargo add serde_json2. Log in and get a token
Send your credentials once and keep the token and expires_at from the response.
bash
curl -X POST "https://your-domain.com/api/v1/users/login" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "your-password"
}'php
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$http = new Client(['base_uri' => 'https://your-domain.com']);
$response = $http->post('/api/v1/users/login', [
'headers' => ['Accept' => 'application/json'],
'json' => [
'email' => '[email protected]',
'password' => 'your-password',
],
]);
$auth = json_decode($response->getBody(), true)['data'];
$token = $auth['token']; // "12|k3jH8sDf..."
$expiresAt = $auth['expires_at']; // "2025-01-02 10:30:00"
$ispId = $auth['user']['isp_id'];
$branchId = $auth['user']['branch_id'];javascript
const BASE_URL = 'https://your-domain.com';
const response = await fetch(`${BASE_URL}/api/v1/users/login`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: '[email protected]',
password: 'your-password',
}),
});
const { data: auth } = await response.json();
const token = auth.token; // "12|k3jH8sDf..."
const expiresAt = auth.expires_at; // "2025-01-02 10:30:00"
const { isp_id: ispId, branch_id: branchId } = auth.user;python
import requests
BASE_URL = "https://your-domain.com"
response = requests.post(
f"{BASE_URL}/api/v1/users/login",
headers={"Accept": "application/json"},
json={
"email": "[email protected]",
"password": "your-password",
},
)
auth = response.json()["data"]
token = auth["token"] # "12|k3jH8sDf..."
expires_at = auth["expires_at"] # "2025-01-02 10:30:00"
isp_id = auth["user"]["isp_id"]
branch_id = auth["user"]["branch_id"]go
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
const baseURL = "https://your-domain.com"
type loginResponse struct {
Data struct {
Token string `json:"token"`
ExpiresAt string `json:"expires_at"`
User struct {
IspID int `json:"isp_id"`
BranchID int `json:"branch_id"`
} `json:"user"`
} `json:"data"`
}
func main() {
payload, _ := json.Marshal(map[string]string{
"email": "[email protected]",
"password": "your-password",
})
req, _ := http.NewRequest("POST", baseURL+"/api/v1/users/login", bytes.NewBuffer(payload))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var auth loginResponse
json.NewDecoder(resp.Body).Decode(&auth)
token := auth.Data.Token // "12|k3jH8sDf..."
expiresAt := auth.Data.ExpiresAt // "2025-01-02 10:30:00"
fmt.Println(token, expiresAt, auth.Data.User.IspID, auth.Data.User.BranchID)
}rust
use reqwest::Client;
use serde_json::{json, Value};
const BASE_URL: &str = "https://your-domain.com";
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let client = Client::new();
let auth: Value = client
.post(format!("{BASE_URL}/api/v1/users/login"))
.header("Accept", "application/json")
.json(&json!({
"email": "[email protected]",
"password": "your-password"
}))
.send()
.await?
.json()
.await?;
let token = auth["data"]["token"].as_str().unwrap(); // "12|k3jH8sDf..."
let expires_at = auth["data"]["expires_at"].as_str().unwrap(); // "2025-01-02 10:30:00"
let isp_id = auth["data"]["user"]["isp_id"].as_i64().unwrap();
let branch_id = auth["data"]["user"]["branch_id"].as_i64().unwrap();
println!("{token} {expires_at} {isp_id} {branch_id}");
Ok(())
}Response:
json
{
"status": "success",
"message": "Login Successful",
"data": {
"user": { "id": 1, "name": "Admin User", "email": "[email protected]", "profile_type": 1, "isp_id": 1, "branch_id": 1 },
"token": "12|k3jH8sDf9aLpQwErTyUiOp1234567890abcdef",
"token_type": "Bearer",
"expires_at": "2025-01-02 10:30:00"
}
}Store the token securely
Treat the token like a password: keep it server-side or in secure storage, never in front-end source code or version control. Login is rate-limited to 5 attempts per minute — cache the token instead of logging in on every request.
3. Call a protected endpoint
Add the Authorization header. GET /api/v1/users/profile is a handy way to check a stored token still works.
bash
curl -X GET "https://your-domain.com/api/v1/users/profile" \
-H "Accept: application/json" \
-H "Authorization: Bearer your-auth-token"php
$response = $http->get('/api/v1/users/profile', [
'headers' => [
'Accept' => 'application/json',
'Authorization' => "Bearer {$token}",
],
]);
$profile = json_decode($response->getBody(), true)['data']['user'];
echo $profile['name'];javascript
const response = await fetch(`${BASE_URL}/api/v1/users/profile`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
},
});
const { data } = await response.json();
console.log(data.user.name);python
response = requests.get(
f"{BASE_URL}/api/v1/users/profile",
headers={
"Accept": "application/json",
"Authorization": f"Bearer {token}",
},
)
profile = response.json()["data"]["user"]
print(profile["name"])go
req, _ := http.NewRequest("GET", baseURL+"/api/v1/users/profile", nil)
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var body map[string]any
json.NewDecoder(resp.Body).Decode(&body)
fmt.Println(body["data"].(map[string]any)["user"].(map[string]any)["name"])rust
let profile: Value = client
.get(format!("{BASE_URL}/api/v1/users/profile"))
.header("Accept", "application/json")
.bearer_auth(token)
.send()
.await?
.json()
.await?;
println!("{}", profile["data"]["user"]["name"]);4. Keep the session alive (refresh and re-login)
The token expires 30 days after login (unless revoked earlier). Two things to handle:
- Before expiry — call
POST /api/v1/users/refresh-token. It returns a new token (and a newexpires_at) and revokes the old one, so overwrite what you stored. - After expiry (or if the token was revoked by a logout) — every request returns
401 Unauthenticated.. Refresh won't work anymore; log in again with the credentials and retry the request.
bash
# Refresh (works only while the current token is still valid)
curl -X POST "https://your-domain.com/api/v1/users/refresh-token" \
-H "Accept: application/json" \
-H "Authorization: Bearer your-auth-token"
# → {"status":"success","data":{"token":"13|Zx9c...","token_type":"Bearer","expires_at":"2025-01-03 10:30:00"}}
# If this returns 401, the token already expired: call /api/v1/users/login again.php
/**
* Returns a valid token, refreshing or re-logging-in as needed.
* $auth is the ['token' => ..., 'expires_at' => ...] array you stored at login.
*/
function ensureToken(Client $http, array &$auth, string $email, string $password): string
{
$expiresAt = new DateTimeImmutable($auth['expires_at']);
// Still comfortably valid
if ($expiresAt > new DateTimeImmutable('+1 day')) {
return $auth['token'];
}
// Less than a day left: try to refresh
$response = $http->post('/api/v1/users/refresh-token', [
'http_errors' => false,
'headers' => [
'Accept' => 'application/json',
'Authorization' => "Bearer {$auth['token']}",
],
]);
if ($response->getStatusCode() === 200) {
$auth = json_decode($response->getBody(), true)['data'];
return $auth['token'];
}
// Already expired or revoked: log in again
$response = $http->post('/api/v1/users/login', [
'headers' => ['Accept' => 'application/json'],
'json' => ['email' => $email, 'password' => $password],
]);
$auth = json_decode($response->getBody(), true)['data'];
return $auth['token'];
}javascript
/**
* Returns a valid token, refreshing or re-logging-in as needed.
* `auth` is the { token, expires_at } object you stored at login.
*/
async function ensureToken(auth, { email, password }) {
const oneDay = 24 * 60 * 60 * 1000;
const expiresAt = new Date(auth.expires_at.replace(' ', 'T'));
// Still comfortably valid
if (expiresAt.getTime() - Date.now() > oneDay) return auth.token;
// Less than a day left: try to refresh
const refresh = await fetch(`${BASE_URL}/api/v1/users/refresh-token`, {
method: 'POST',
headers: { Accept: 'application/json', Authorization: `Bearer ${auth.token}` },
});
if (refresh.ok) {
Object.assign(auth, (await refresh.json()).data);
return auth.token;
}
// Already expired or revoked: log in again
const login = await fetch(`${BASE_URL}/api/v1/users/login`, {
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
Object.assign(auth, (await login.json()).data);
return auth.token;
}python
from datetime import datetime, timedelta
def ensure_token(auth: dict, email: str, password: str) -> str:
"""Return a valid token, refreshing or re-logging-in as needed.
`auth` is the {"token": ..., "expires_at": ...} dict stored at login;
it is updated in place.
"""
expires_at = datetime.strptime(auth["expires_at"], "%Y-%m-%d %H:%M:%S")
# Still comfortably valid
if expires_at - datetime.now() > timedelta(days=1):
return auth["token"]
# Less than a day left: try to refresh
refresh = requests.post(
f"{BASE_URL}/api/v1/users/refresh-token",
headers={"Accept": "application/json", "Authorization": f"Bearer {auth['token']}"},
)
if refresh.status_code == 200:
auth.update(refresh.json()["data"])
return auth["token"]
# Already expired or revoked: log in again
login = requests.post(
f"{BASE_URL}/api/v1/users/login",
headers={"Accept": "application/json"},
json={"email": email, "password": password},
)
auth.update(login.json()["data"])
return auth["token"]go
type Auth struct {
Token string `json:"token"`
ExpiresAt string `json:"expires_at"`
}
// ensureToken returns a valid token, refreshing or re-logging-in as needed.
func ensureToken(auth *Auth, email, password string) (string, error) {
expiresAt, _ := time.ParseInLocation("2006-01-02 15:04:05", auth.ExpiresAt, time.Local)
// Still comfortably valid
if time.Until(expiresAt) > 24*time.Hour {
return auth.Token, nil
}
// Less than a day left: try to refresh
req, _ := http.NewRequest("POST", baseURL+"/api/v1/users/refresh-token", nil)
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+auth.Token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var body struct {
Data Auth `json:"data"`
}
if resp.StatusCode == http.StatusOK {
json.NewDecoder(resp.Body).Decode(&body)
*auth = body.Data
return auth.Token, nil
}
// Already expired or revoked: log in again
payload, _ := json.Marshal(map[string]string{"email": email, "password": password})
req, _ = http.NewRequest("POST", baseURL+"/api/v1/users/login", bytes.NewBuffer(payload))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
resp, err = http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("login failed: HTTP %d", resp.StatusCode)
}
json.NewDecoder(resp.Body).Decode(&body)
*auth = body.Data
return auth.Token, nil
}rust
use chrono::{Duration, NaiveDateTime, Local};
#[derive(Debug, Clone, serde::Deserialize)]
struct Auth {
token: String,
expires_at: String,
}
/// Returns a valid token, refreshing or re-logging-in as needed.
async fn ensure_token(
client: &Client,
auth: &mut Auth,
email: &str,
password: &str,
) -> Result<String, reqwest::Error> {
let expires_at = NaiveDateTime::parse_from_str(&auth.expires_at, "%Y-%m-%d %H:%M:%S")
.expect("expires_at format");
// Still comfortably valid
if expires_at - Local::now().naive_local() > Duration::days(1) {
return Ok(auth.token.clone());
}
// Less than a day left: try to refresh
let refresh = client
.post(format!("{BASE_URL}/api/v1/users/refresh-token"))
.header("Accept", "application/json")
.bearer_auth(&auth.token)
.send()
.await?;
if refresh.status().is_success() {
let body: Value = refresh.json().await?;
*auth = serde_json::from_value(body["data"].clone()).expect("auth payload");
return Ok(auth.token.clone());
}
// Already expired or revoked: log in again
let body: Value = client
.post(format!("{BASE_URL}/api/v1/users/login"))
.header("Accept", "application/json")
.json(&json!({ "email": email, "password": password }))
.send()
.await?
.json()
.await?;
*auth = serde_json::from_value(body["data"].clone()).expect("auth payload");
Ok(auth.token.clone())
}Why check expires_at client-side instead of just retrying on 401?
Both. Refreshing proactively avoids a failed request and a re-login round-trip in the middle of a batch job. Retrying on 401 covers the cases you can't predict — a token revoked by Logout All Devices, or clock drift between your machine and the server (expires_at is server time). The complete client does both.
5. Create a subscriber
POST /api/v1/subscribers/create — the seven required fields are shown; see the full field list for the optional ones (static IP, MAC, address, area, quotas…).
A 422 response means validation failed: errors maps each field to its messages, and message explains business-rule failures such as Username already exists or Package not assigned to this salesperson.
bash
curl -X POST "https://your-domain.com/api/v1/subscribers/create" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-auth-token" \
-d '{
"isp_id": 1,
"branch_id": 1,
"username": "subscriber001",
"fullname": "John Doe",
"password": "pass123",
"package_id": 1,
"salesperson_id": 1,
"phone": "01712345678",
"email": "[email protected]"
}'php
$response = $http->post('/api/v1/subscribers/create', [
'http_errors' => false,
'headers' => [
'Accept' => 'application/json',
'Authorization' => "Bearer {$token}",
],
'json' => [
'isp_id' => $ispId,
'branch_id' => $branchId,
'username' => 'subscriber001',
'fullname' => 'John Doe',
'password' => 'pass123',
'package_id' => 1,
'salesperson_id' => 1,
'phone' => '01712345678',
'email' => '[email protected]',
],
]);
$body = json_decode($response->getBody(), true);
if ($response->getStatusCode() === 422) {
// Field errors: ['username' => ['The username field is required.'], ...]
foreach ($body['errors'] ?? [] as $field => $messages) {
echo "{$field}: " . implode(', ', $messages) . PHP_EOL;
}
// Business-rule errors (duplicate username, package not assigned...) come in 'message'
exit("Validation failed: " . json_encode($body['message']) . PHP_EOL);
}
echo "Created subscriber #{$body['data']['id']}" . PHP_EOL;javascript
const response = await fetch(`${BASE_URL}/api/v1/subscribers/create`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
isp_id: ispId,
branch_id: branchId,
username: 'subscriber001',
fullname: 'John Doe',
password: 'pass123',
package_id: 1,
salesperson_id: 1,
phone: '01712345678',
email: '[email protected]',
}),
});
const body = await response.json();
if (response.status === 422) {
// Field errors: { username: ['The username field is required.'], ... }
for (const [field, messages] of Object.entries(body.errors ?? {})) {
console.error(`${field}: ${messages.join(', ')}`);
}
// Business-rule errors (duplicate username, package not assigned...) come in `message`
throw new Error(`Validation failed: ${JSON.stringify(body.message)}`);
}
console.log(`Created subscriber #${body.data.id}`);python
response = requests.post(
f"{BASE_URL}/api/v1/subscribers/create",
headers={
"Accept": "application/json",
"Authorization": f"Bearer {token}",
},
json={
"isp_id": isp_id,
"branch_id": branch_id,
"username": "subscriber001",
"fullname": "John Doe",
"password": "pass123",
"package_id": 1,
"salesperson_id": 1,
"phone": "01712345678",
"email": "[email protected]",
},
)
body = response.json()
if response.status_code == 422:
# Field errors: {"username": ["The username field is required."], ...}
for field, messages in body.get("errors", {}).items():
print(f"{field}: {', '.join(messages)}")
# Business-rule errors (duplicate username, package not assigned...) come in "message"
raise SystemExit(f"Validation failed: {body['message']}")
print(f"Created subscriber #{body['data']['id']}")go
payload, _ := json.Marshal(map[string]any{
"isp_id": ispID,
"branch_id": branchID,
"username": "subscriber001",
"fullname": "John Doe",
"password": "pass123",
"package_id": 1,
"salesperson_id": 1,
"phone": "01712345678",
"email": "[email protected]",
})
req, _ := http.NewRequest("POST", baseURL+"/api/v1/subscribers/create", bytes.NewBuffer(payload))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var body struct {
Message any `json:"message"`
Errors map[string][]string `json:"errors"`
Data struct {
ID int `json:"id"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&body)
if resp.StatusCode == http.StatusUnprocessableEntity {
// Field errors: {"username": ["The username field is required."], ...}
for field, messages := range body.Errors {
fmt.Printf("%s: %s\n", field, strings.Join(messages, ", "))
}
// Business-rule errors (duplicate username, package not assigned...) come in "message"
panic(fmt.Sprintf("validation failed: %v", body.Message))
}
fmt.Printf("Created subscriber #%d\n", body.Data.ID)rust
let response = client
.post(format!("{BASE_URL}/api/v1/subscribers/create"))
.header("Accept", "application/json")
.bearer_auth(token)
.json(&json!({
"isp_id": isp_id,
"branch_id": branch_id,
"username": "subscriber001",
"fullname": "John Doe",
"password": "pass123",
"package_id": 1,
"salesperson_id": 1,
"phone": "01712345678",
"email": "[email protected]"
}))
.send()
.await?;
let status = response.status();
let body: Value = response.json().await?;
if status == reqwest::StatusCode::UNPROCESSABLE_ENTITY {
// Field errors: {"username": ["The username field is required."], ...}
if let Some(errors) = body["errors"].as_object() {
for (field, messages) in errors {
eprintln!("{field}: {messages}");
}
}
// Business-rule errors (duplicate username, package not assigned...) come in "message"
panic!("validation failed: {}", body["message"]);
}
println!("Created subscriber #{}", body["data"]["id"]);Success response (201 Created) — data is the full subscriber record:
json
{
"status": "success",
"message": "Subscriber created successfully.",
"data": {
"id": 151,
"username": "subscriber001",
"fullname": "John Doe",
"package_id": 1,
"salesperson_id": 1,
"profile_status": 0,
"isp_id": 1,
"branch_id": 1,
"created_at": "2025-01-01T10:30:00.000000Z"
}
}6. List subscribers with pagination
GET /api/v1/subscribers returns a JSON array of subscribers. Pass both offset and limit to page through it (without them the first 100 rows are returned); keep going until a page comes back with fewer than limit rows. Add package_module=1, balance_module=1, … to include related data — see Fetch All Subscribers for every filter.
bash
# Page 1
curl -X GET "https://your-domain.com/api/v1/subscribers?isp_id=1&branch_id=1&offset=0&limit=100" \
-H "Accept: application/json" \
-H "Authorization: Bearer your-auth-token"
# Page 2
curl -X GET "https://your-domain.com/api/v1/subscribers?isp_id=1&branch_id=1&offset=100&limit=100" \
-H "Accept: application/json" \
-H "Authorization: Bearer your-auth-token"php
$limit = 100;
$offset = 0;
$all = [];
do {
$response = $http->get('/api/v1/subscribers', [
'headers' => [
'Accept' => 'application/json',
'Authorization' => "Bearer {$token}",
],
'query' => [
'isp_id' => $ispId,
'branch_id' => $branchId,
'offset' => $offset,
'limit' => $limit,
],
]);
$page = json_decode($response->getBody(), true);
$all = array_merge($all, $page);
$offset += $limit;
} while (count($page) === $limit);
echo count($all) . " subscribers" . PHP_EOL;javascript
const limit = 100;
let offset = 0;
let page;
const all = [];
do {
const params = new URLSearchParams({ isp_id: ispId, branch_id: branchId, offset, limit });
const response = await fetch(`${BASE_URL}/api/v1/subscribers?${params}`, {
headers: { Accept: 'application/json', Authorization: `Bearer ${token}` },
});
page = await response.json();
all.push(...page);
offset += limit;
} while (page.length === limit);
console.log(`${all.length} subscribers`);python
limit = 100
offset = 0
all_subscribers = []
while True:
response = requests.get(
f"{BASE_URL}/api/v1/subscribers",
headers={"Accept": "application/json", "Authorization": f"Bearer {token}"},
params={"isp_id": isp_id, "branch_id": branch_id, "offset": offset, "limit": limit},
)
page = response.json()
all_subscribers.extend(page)
offset += limit
if len(page) < limit:
break
print(f"{len(all_subscribers)} subscribers")go
const limit = 100
offset := 0
var all []map[string]any
for {
q := url.Values{}
q.Set("isp_id", strconv.Itoa(ispID))
q.Set("branch_id", strconv.Itoa(branchID))
q.Set("offset", strconv.Itoa(offset))
q.Set("limit", strconv.Itoa(limit))
req, _ := http.NewRequest("GET", baseURL+"/api/v1/subscribers?"+q.Encode(), nil)
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
var page []map[string]any
json.NewDecoder(resp.Body).Decode(&page)
resp.Body.Close()
all = append(all, page...)
offset += limit
if len(page) < limit {
break
}
}
fmt.Printf("%d subscribers\n", len(all))rust
let limit = 100;
let mut offset = 0;
let mut all: Vec<Value> = Vec::new();
loop {
let body: Value = client
.get(format!("{BASE_URL}/api/v1/subscribers"))
.header("Accept", "application/json")
.bearer_auth(token)
.query(&[
("isp_id", isp_id.to_string()),
("branch_id", branch_id.to_string()),
("offset", offset.to_string()),
("limit", limit.to_string()),
])
.send()
.await?
.json()
.await?;
let page = body.as_array().cloned().unwrap_or_default();
let count = page.len();
all.extend(page);
offset += limit;
if count < limit {
break;
}
}
println!("{} subscribers", all.len());7. Complete client
Everything above in one reusable piece: it logs in lazily, refreshes the token a day before expiry, retries once after a 401 by logging in again, and turns non-2xx responses into a typed error carrying the API's message and errors.
php
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use Psr\Http\Message\ResponseInterface;
class ZalUltraException extends RuntimeException
{
public function __construct(public readonly int $status, public readonly array $body)
{
$message = $body['message'] ?? "HTTP {$status}";
parent::__construct(is_array($message) ? implode(' ', $message) : (string) $message, $status);
}
/** Field-level validation errors from a 422 response */
public function errors(): array
{
return $this->body['errors'] ?? [];
}
}
class ZalUltraClient
{
private Client $http;
private ?string $token = null;
private ?DateTimeImmutable $expiresAt = null;
public function __construct(
string $baseUrl,
private string $email,
private string $password,
) {
$this->http = new Client(['base_uri' => $baseUrl, 'http_errors' => false]);
}
// --- Auth -------------------------------------------------------------
/** @return array the logged-in user (id, name, email, profile_type, isp_id, branch_id) */
public function login(): array
{
$response = $this->http->post('/api/v1/users/login', [
'headers' => ['Accept' => 'application/json'],
'json' => ['email' => $this->email, 'password' => $this->password],
]);
$body = $this->decode($response);
$this->storeToken($body['data']);
return $body['data']['user'];
}
public function refreshToken(): void
{
$response = $this->http->post('/api/v1/users/refresh-token', [
'headers' => $this->authHeaders(),
]);
if ($response->getStatusCode() === 200) {
$this->storeToken(json_decode($response->getBody(), true)['data']);
} else {
$this->login(); // token already expired or revoked
}
}
public function logout(): void
{
$this->http->post('/api/v1/users/logout', ['headers' => $this->authHeaders()]);
$this->token = $this->expiresAt = null;
}
// --- Generic request with automatic token handling --------------------
/**
* @param array $options Guzzle options: 'query' for GET params, 'json' for a body
*/
public function request(string $method, string $path, array $options = []): array
{
if ($this->token === null) {
$this->login();
} elseif ($this->expiresAt < new DateTimeImmutable('+1 day')) {
$this->refreshToken();
}
$response = $this->send($method, $path, $options);
if ($response->getStatusCode() === 401) {
// Revoked, or clock drift: log in again and retry once
$this->login();
$response = $this->send($method, $path, $options);
}
return $this->decode($response);
}
// --- Convenience wrappers ---------------------------------------------
public function profile(): array
{
return $this->request('GET', '/api/v1/users/profile')['data']['user'];
}
public function createSubscriber(array $subscriber): array
{
return $this->request('POST', '/api/v1/subscribers/create', ['json' => $subscriber])['data'];
}
/** Iterates over every subscriber matching $filters, fetching pages as needed. */
public function subscribers(int $ispId, int $branchId, array $filters = [], int $limit = 100): Generator
{
$offset = 0;
do {
$page = $this->request('GET', '/api/v1/subscribers', [
'query' => $filters + [
'isp_id' => $ispId, 'branch_id' => $branchId, 'offset' => $offset, 'limit' => $limit,
],
]);
yield from $page;
$offset += $limit;
} while (count($page) === $limit);
}
// --- Internals ----------------------------------------------------------
private function send(string $method, string $path, array $options): ResponseInterface
{
$options['headers'] = ($options['headers'] ?? []) + $this->authHeaders();
return $this->http->request($method, $path, $options);
}
private function authHeaders(): array
{
return ['Accept' => 'application/json', 'Authorization' => "Bearer {$this->token}"];
}
private function storeToken(array $data): void
{
$this->token = $data['token'];
$this->expiresAt = new DateTimeImmutable($data['expires_at']);
}
private function decode(ResponseInterface $response): array
{
$body = json_decode((string) $response->getBody(), true) ?? [];
if ($response->getStatusCode() >= 400) {
throw new ZalUltraException($response->getStatusCode(), $body);
}
return $body;
}
}
// --- Usage ------------------------------------------------------------------
$zal = new ZalUltraClient('https://your-domain.com', '[email protected]', 'your-password');
$user = $zal->login();
try {
$created = $zal->createSubscriber([
'isp_id' => $user['isp_id'],
'branch_id' => $user['branch_id'],
'username' => 'subscriber001',
'fullname' => 'John Doe',
'password' => 'pass123',
'package_id' => 1,
'salesperson_id' => $user['id'],
]);
echo "Created subscriber #{$created['id']}\n";
} catch (ZalUltraException $e) {
echo "Failed ({$e->status}): {$e->getMessage()}\n";
print_r($e->errors());
}
foreach ($zal->subscribers($user['isp_id'], $user['branch_id'], ['subscriber_type' => 2]) as $subscriber) {
echo "{$subscriber['username']} expires {$subscriber['expiration_date']}\n";
}javascript
// zal-ultra.js — works in Node.js 18+ (native fetch) and modern browsers/bundlers.
export class ZalUltraError extends Error {
constructor(status, body) {
const message = body?.message ?? `HTTP ${status}`;
super(Array.isArray(message) ? message.join(' ') : String(message));
this.name = 'ZalUltraError';
this.status = status;
/** Field-level validation errors from a 422 response */
this.errors = body?.errors ?? {};
}
}
export class ZalUltraClient {
#token = null;
#expiresAt = null;
constructor(baseUrl, email, password) {
this.baseUrl = baseUrl.replace(/\/$/, '');
this.email = email;
this.password = password;
}
// --- Auth -------------------------------------------------------------
/** @returns the logged-in user (id, name, email, profile_type, isp_id, branch_id) */
async login() {
const response = await fetch(`${this.baseUrl}/api/v1/users/login`, {
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify({ email: this.email, password: this.password }),
});
const body = await this.#decode(response);
this.#storeToken(body.data);
return body.data.user;
}
async refreshToken() {
const response = await fetch(`${this.baseUrl}/api/v1/users/refresh-token`, {
method: 'POST',
headers: this.#authHeaders(),
});
if (response.ok) {
this.#storeToken((await response.json()).data);
} else {
await this.login(); // token already expired or revoked
}
}
async logout() {
await fetch(`${this.baseUrl}/api/v1/users/logout`, { method: 'POST', headers: this.#authHeaders() });
this.#token = this.#expiresAt = null;
}
// --- Generic request with automatic token handling --------------------
/**
* @param {object} [options]
* @param {object} [options.query] URL query parameters
* @param {object} [options.json] JSON request body
*/
async request(method, path, { query, json } = {}) {
if (!this.#token) {
await this.login();
} else if (this.#expiresAt - Date.now() < 24 * 60 * 60 * 1000) {
await this.refreshToken();
}
let response = await this.#send(method, path, query, json);
if (response.status === 401) {
// Revoked, or clock drift: log in again and retry once
await this.login();
response = await this.#send(method, path, query, json);
}
return this.#decode(response);
}
// --- Convenience wrappers ---------------------------------------------
async profile() {
return (await this.request('GET', '/api/v1/users/profile')).data.user;
}
async createSubscriber(subscriber) {
return (await this.request('POST', '/api/v1/subscribers/create', { json: subscriber })).data;
}
/** Async-iterates over every subscriber matching `filters`, fetching pages as needed. */
async *subscribers(ispId, branchId, filters = {}, limit = 100) {
let offset = 0;
let page;
do {
page = await this.request('GET', '/api/v1/subscribers', {
query: { ...filters, isp_id: ispId, branch_id: branchId, offset, limit },
});
yield* page;
offset += limit;
} while (page.length === limit);
}
// --- Internals ----------------------------------------------------------
#send(method, path, query, json) {
const url = new URL(this.baseUrl + path);
for (const [k, v] of Object.entries(query ?? {})) url.searchParams.set(k, v);
return fetch(url, {
method,
headers: { ...this.#authHeaders(), ...(json && { 'Content-Type': 'application/json' }) },
body: json ? JSON.stringify(json) : undefined,
});
}
#authHeaders() {
return { Accept: 'application/json', Authorization: `Bearer ${this.#token}` };
}
#storeToken(data) {
this.#token = data.token;
this.#expiresAt = new Date(data.expires_at.replace(' ', 'T')).getTime();
}
async #decode(response) {
const body = await response.json().catch(() => ({}));
if (!response.ok) throw new ZalUltraError(response.status, body);
return body;
}
}
// --- Usage ------------------------------------------------------------------
const zal = new ZalUltraClient('https://your-domain.com', '[email protected]', 'your-password');
const user = await zal.login();
try {
const created = await zal.createSubscriber({
isp_id: user.isp_id,
branch_id: user.branch_id,
username: 'subscriber001',
fullname: 'John Doe',
password: 'pass123',
package_id: 1,
salesperson_id: user.id,
});
console.log(`Created subscriber #${created.id}`);
} catch (e) {
if (e instanceof ZalUltraError) console.error(`Failed (${e.status}): ${e.message}`, e.errors);
else throw e;
}
for await (const subscriber of zal.subscribers(user.isp_id, user.branch_id, { subscriber_type: 2 })) {
console.log(`${subscriber.username} expires ${subscriber.expiration_date}`);
}python
"""zal_ultra.py — a small client for the Zal Ultra API (requires `requests`)."""
from datetime import datetime, timedelta
from typing import Any, Iterator
import requests
class ZalUltraError(Exception):
def __init__(self, status: int, body: dict):
message = body.get("message", f"HTTP {status}")
super().__init__(" ".join(message) if isinstance(message, list) else str(message))
self.status = status
self.errors: dict[str, list[str]] = body.get("errors", {}) # field errors from a 422
class ZalUltraClient:
def __init__(self, base_url: str, email: str, password: str):
self.base_url = base_url.rstrip("/")
self.email = email
self.password = password
self._token: str | None = None
self._expires_at: datetime | None = None
self._http = requests.Session()
self._http.headers["Accept"] = "application/json"
# --- Auth -------------------------------------------------------------
def login(self) -> dict:
"""Log in and return the user (id, name, email, profile_type, isp_id, branch_id)."""
response = self._http.post(
f"{self.base_url}/api/v1/users/login",
json={"email": self.email, "password": self.password},
)
body = self._decode(response)
self._store_token(body["data"])
return body["data"]["user"]
def refresh_token(self) -> None:
response = self._http.post(f"{self.base_url}/api/v1/users/refresh-token", headers=self._auth())
if response.status_code == 200:
self._store_token(response.json()["data"])
else:
self.login() # token already expired or revoked
def logout(self) -> None:
self._http.post(f"{self.base_url}/api/v1/users/logout", headers=self._auth())
self._token = self._expires_at = None
# --- Generic request with automatic token handling --------------------
def request(self, method: str, path: str, *, params: dict | None = None, json: Any = None) -> Any:
if self._token is None:
self.login()
elif self._expires_at - datetime.now() < timedelta(days=1):
self.refresh_token()
response = self._send(method, path, params, json)
if response.status_code == 401:
# Revoked, or clock drift: log in again and retry once
self.login()
response = self._send(method, path, params, json)
return self._decode(response)
# --- Convenience wrappers ---------------------------------------------
def profile(self) -> dict:
return self.request("GET", "/api/v1/users/profile")["data"]["user"]
def create_subscriber(self, subscriber: dict) -> dict:
return self.request("POST", "/api/v1/subscribers/create", json=subscriber)["data"]
def subscribers(self, isp_id: int, branch_id: int, limit: int = 100, **filters) -> Iterator[dict]:
"""Iterate over every subscriber matching `filters`, fetching pages as needed."""
offset = 0
while True:
page = self.request(
"GET",
"/api/v1/subscribers",
params={**filters, "isp_id": isp_id, "branch_id": branch_id, "offset": offset, "limit": limit},
)
yield from page
offset += limit
if len(page) < limit:
return
# --- Internals ----------------------------------------------------------
def _send(self, method: str, path: str, params: dict | None, json: Any) -> requests.Response:
return self._http.request(method, f"{self.base_url}{path}", params=params, json=json, headers=self._auth())
def _auth(self) -> dict:
return {"Authorization": f"Bearer {self._token}"}
def _store_token(self, data: dict) -> None:
self._token = data["token"]
self._expires_at = datetime.strptime(data["expires_at"], "%Y-%m-%d %H:%M:%S")
@staticmethod
def _decode(response: requests.Response) -> Any:
try:
body = response.json()
except ValueError:
body = {}
if response.status_code >= 400:
raise ZalUltraError(response.status_code, body if isinstance(body, dict) else {})
return body
# --- Usage ------------------------------------------------------------------
if __name__ == "__main__":
zal = ZalUltraClient("https://your-domain.com", "[email protected]", "your-password")
user = zal.login()
try:
created = zal.create_subscriber({
"isp_id": user["isp_id"],
"branch_id": user["branch_id"],
"username": "subscriber001",
"fullname": "John Doe",
"password": "pass123",
"package_id": 1,
"salesperson_id": user["id"],
})
print(f"Created subscriber #{created['id']}")
except ZalUltraError as e:
print(f"Failed ({e.status}): {e}", e.errors)
for subscriber in zal.subscribers(user["isp_id"], user["branch_id"], subscriber_type=2):
print(f"{subscriber['username']} expires {subscriber['expiration_date']}")go
// zalultra.go — a small client for the Zal Ultra API (standard library only).
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
)
// APIError carries the API's status code, message and 422 field errors.
type APIError struct {
Status int `json:"-"`
Message any `json:"message"`
Errors map[string][]string `json:"errors"`
}
func (e *APIError) Error() string { return fmt.Sprintf("HTTP %d: %v", e.Status, e.Message) }
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
ProfileType int `json:"profile_type"`
IspID int `json:"isp_id"`
BranchID int `json:"branch_id"`
}
type Client struct {
BaseURL string
email string
password string
http *http.Client
token string
expiresAt time.Time
}
func New(baseURL, email, password string) *Client {
return &Client{BaseURL: baseURL, email: email, password: password, http: &http.Client{Timeout: 30 * time.Second}}
}
// --- Auth ---------------------------------------------------------------------
// Login authenticates and returns the user (id, name, email, profile_type, isp_id, branch_id).
func (c *Client) Login() (*User, error) {
var body struct {
Data struct {
User User `json:"user"`
Token string `json:"token"`
ExpiresAt string `json:"expires_at"`
} `json:"data"`
}
req := c.newRequest("POST", "/api/v1/users/login", nil, map[string]string{"email": c.email, "password": c.password})
if err := c.do(req, &body); err != nil {
return nil, err
}
c.storeToken(body.Data.Token, body.Data.ExpiresAt)
return &body.Data.User, nil
}
func (c *Client) RefreshToken() error {
var body struct {
Data struct {
Token string `json:"token"`
ExpiresAt string `json:"expires_at"`
} `json:"data"`
}
req := c.newRequest("POST", "/api/v1/users/refresh-token", nil, nil)
req.Header.Set("Authorization", "Bearer "+c.token)
if err := c.do(req, &body); err != nil {
_, err = c.Login() // token already expired or revoked
return err
}
c.storeToken(body.Data.Token, body.Data.ExpiresAt)
return nil
}
func (c *Client) Logout() error {
req := c.newRequest("POST", "/api/v1/users/logout", nil, nil)
req.Header.Set("Authorization", "Bearer "+c.token)
err := c.do(req, nil)
c.token, c.expiresAt = "", time.Time{}
return err
}
// --- Generic request with automatic token handling ----------------------------
// Request performs an authenticated call and decodes the JSON response into out.
func (c *Client) Request(method, path string, query url.Values, body any, out any) error {
if c.token == "" {
if _, err := c.Login(); err != nil {
return err
}
} else if time.Until(c.expiresAt) < 24*time.Hour {
if err := c.RefreshToken(); err != nil {
return err
}
}
req := c.newRequest(method, path, query, body)
req.Header.Set("Authorization", "Bearer "+c.token)
err := c.do(req, out)
if apiErr, ok := err.(*APIError); ok && apiErr.Status == http.StatusUnauthorized {
// Revoked, or clock drift: log in again and retry once
if _, err := c.Login(); err != nil {
return err
}
req = c.newRequest(method, path, query, body)
req.Header.Set("Authorization", "Bearer "+c.token)
err = c.do(req, out)
}
return err
}
// --- Convenience wrappers -----------------------------------------------------
func (c *Client) Profile() (*User, error) {
var body struct {
Data struct {
User User `json:"user"`
} `json:"data"`
}
err := c.Request("GET", "/api/v1/users/profile", nil, nil, &body)
return &body.Data.User, err
}
func (c *Client) CreateSubscriber(subscriber map[string]any) (int, error) {
var body struct {
Data struct {
ID int `json:"id"`
} `json:"data"`
}
err := c.Request("POST", "/api/v1/subscribers/create", nil, subscriber, &body)
return body.Data.ID, err
}
// Subscribers fetches every subscriber matching filters, page by page.
func (c *Client) Subscribers(ispID, branchID int, filters url.Values) ([]map[string]any, error) {
const limit = 100
var all []map[string]any
for offset := 0; ; offset += limit {
q := url.Values{}
for k, v := range filters {
q[k] = v
}
q.Set("isp_id", strconv.Itoa(ispID))
q.Set("branch_id", strconv.Itoa(branchID))
q.Set("offset", strconv.Itoa(offset))
q.Set("limit", strconv.Itoa(limit))
var page []map[string]any
if err := c.Request("GET", "/api/v1/subscribers", q, nil, &page); err != nil {
return all, err
}
all = append(all, page...)
if len(page) < limit {
return all, nil
}
}
}
// --- Internals ------------------------------------------------------------------
func (c *Client) newRequest(method, path string, query url.Values, body any) *http.Request {
endpoint := c.BaseURL + path
if len(query) > 0 {
endpoint += "?" + query.Encode()
}
var payload *bytes.Buffer
if body != nil {
b, _ := json.Marshal(body)
payload = bytes.NewBuffer(b)
} else {
payload = &bytes.Buffer{}
}
req, _ := http.NewRequest(method, endpoint, payload)
req.Header.Set("Accept", "application/json")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
return req
}
func (c *Client) do(req *http.Request, out any) error {
resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
apiErr := &APIError{Status: resp.StatusCode}
json.NewDecoder(resp.Body).Decode(apiErr)
return apiErr
}
if out != nil {
return json.NewDecoder(resp.Body).Decode(out)
}
return nil
}
func (c *Client) storeToken(token, expiresAt string) {
c.token = token
c.expiresAt, _ = time.ParseInLocation("2006-01-02 15:04:05", expiresAt, time.Local)
}
// --- Usage ----------------------------------------------------------------------
func main() {
zal := New("https://your-domain.com", "[email protected]", "your-password")
user, err := zal.Login()
if err != nil {
panic(err)
}
id, err := zal.CreateSubscriber(map[string]any{
"isp_id": user.IspID,
"branch_id": user.BranchID,
"username": "subscriber001",
"fullname": "John Doe",
"password": "pass123",
"package_id": 1,
"salesperson_id": user.ID,
})
if apiErr, ok := err.(*APIError); ok {
fmt.Printf("Failed (%d): %v %v\n", apiErr.Status, apiErr.Message, apiErr.Errors)
} else if err != nil {
panic(err)
} else {
fmt.Printf("Created subscriber #%d\n", id)
}
subscribers, err := zal.Subscribers(user.IspID, user.BranchID, url.Values{"subscriber_type": {"2"}})
if err != nil {
panic(err)
}
for _, s := range subscribers {
fmt.Printf("%v expires %v\n", s["username"], s["expiration_date"])
}
}rust
// zal_ultra.rs — a small client for the Zal Ultra API.
//
// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde = { version = "1", features = ["derive"] }
// serde_json = "1"
// chrono = "0.4"
use chrono::{Duration, Local, NaiveDateTime};
use reqwest::{Method, StatusCode};
use serde::Deserialize;
use serde_json::{json, Value};
use std::collections::HashMap;
#[derive(Debug)]
pub enum Error {
Http(reqwest::Error),
/// Non-2xx response: status, `message`, and 422 field `errors`
Api { status: StatusCode, message: Value, errors: HashMap<String, Vec<String>> },
}
impl From<reqwest::Error> for Error {
fn from(e: reqwest::Error) -> Self { Error::Http(e) }
}
#[derive(Debug, Clone, Deserialize)]
pub struct User {
pub id: i64,
pub name: String,
pub email: String,
pub profile_type: i64,
pub isp_id: i64,
pub branch_id: i64,
}
pub struct Client {
base_url: String,
email: String,
password: String,
http: reqwest::Client,
token: Option<String>,
expires_at: Option<NaiveDateTime>,
}
impl Client {
pub fn new(base_url: &str, email: &str, password: &str) -> Self {
Self {
base_url: base_url.trim_end_matches('/').to_string(),
email: email.to_string(),
password: password.to_string(),
http: reqwest::Client::new(),
token: None,
expires_at: None,
}
}
// --- Auth ---------------------------------------------------------------
/// Log in and return the user (id, name, email, profile_type, isp_id, branch_id).
pub async fn login(&mut self) -> Result<User, Error> {
let body = self
.send(Method::POST, "/api/v1/users/login", None, Some(json!({
"email": self.email, "password": self.password
})), false)
.await?;
self.store_token(&body["data"]);
Ok(serde_json::from_value(body["data"]["user"].clone()).expect("user payload"))
}
pub async fn refresh_token(&mut self) -> Result<(), Error> {
match self.send(Method::POST, "/api/v1/users/refresh-token", None, None, true).await {
Ok(body) => { self.store_token(&body["data"]); Ok(()) }
Err(_) => self.login().await.map(|_| ()), // token already expired or revoked
}
}
pub async fn logout(&mut self) -> Result<(), Error> {
self.send(Method::POST, "/api/v1/users/logout", None, None, true).await?;
self.token = None;
self.expires_at = None;
Ok(())
}
// --- Generic request with automatic token handling ----------------------
pub async fn request(
&mut self,
method: Method,
path: &str,
query: Option<&[(&str, String)]>,
body: Option<Value>,
) -> Result<Value, Error> {
match self.expires_at {
None => self.login().await.map(|_| ())?,
Some(exp) if exp - Local::now().naive_local() < Duration::days(1) => self.refresh_token().await?,
_ => {}
}
match self.send(method.clone(), path, query, body.clone(), true).await {
Err(Error::Api { status, .. }) if status == StatusCode::UNAUTHORIZED => {
// Revoked, or clock drift: log in again and retry once
self.login().await?;
self.send(method, path, query, body, true).await
}
other => other,
}
}
// --- Convenience wrappers -----------------------------------------------
pub async fn profile(&mut self) -> Result<User, Error> {
let body = self.request(Method::GET, "/api/v1/users/profile", None, None).await?;
Ok(serde_json::from_value(body["data"]["user"].clone()).expect("user payload"))
}
pub async fn create_subscriber(&mut self, subscriber: Value) -> Result<Value, Error> {
let body = self.request(Method::POST, "/api/v1/subscribers/create", None, Some(subscriber)).await?;
Ok(body["data"].clone())
}
/// Fetch every subscriber matching `filters`, page by page.
pub async fn subscribers(
&mut self,
isp_id: i64,
branch_id: i64,
filters: &[(&str, String)],
) -> Result<Vec<Value>, Error> {
let limit = 100;
let mut offset = 0;
let mut all = Vec::new();
loop {
let mut query: Vec<(&str, String)> = filters.to_vec();
query.extend([
("isp_id", isp_id.to_string()),
("branch_id", branch_id.to_string()),
("offset", offset.to_string()),
("limit", limit.to_string()),
]);
let body = self.request(Method::GET, "/api/v1/subscribers", Some(&query[..]), None).await?;
let page = body.as_array().cloned().unwrap_or_default();
let count = page.len();
all.extend(page);
offset += limit;
if count < limit {
return Ok(all);
}
}
}
// --- Internals ------------------------------------------------------------
async fn send(
&self,
method: Method,
path: &str,
query: Option<&[(&str, String)]>,
body: Option<Value>,
auth: bool,
) -> Result<Value, Error> {
let mut req = self
.http
.request(method, format!("{}{}", self.base_url, path))
.header("Accept", "application/json");
if let Some(q) = query {
req = req.query(q);
}
if let Some(b) = body {
req = req.json(&b);
}
if auth {
req = req.bearer_auth(self.token.as_deref().unwrap_or_default());
}
let response = req.send().await?;
let status = response.status();
let body: Value = response.json().await.unwrap_or(Value::Null);
if status.is_success() {
Ok(body)
} else {
Err(Error::Api {
status,
message: body["message"].clone(),
errors: serde_json::from_value(body["errors"].clone()).unwrap_or_default(),
})
}
}
fn store_token(&mut self, data: &Value) {
self.token = data["token"].as_str().map(str::to_string);
self.expires_at = data["expires_at"]
.as_str()
.and_then(|s| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S").ok());
}
}
// --- Usage ----------------------------------------------------------------------
#[tokio::main]
async fn main() -> Result<(), Error> {
let mut zal = Client::new("https://your-domain.com", "[email protected]", "your-password");
let user = zal.login().await?;
match zal
.create_subscriber(json!({
"isp_id": user.isp_id,
"branch_id": user.branch_id,
"username": "subscriber001",
"fullname": "John Doe",
"password": "pass123",
"package_id": 1,
"salesperson_id": user.id
}))
.await
{
Ok(created) => println!("Created subscriber #{}", created["id"]),
Err(Error::Api { status, message, errors }) => eprintln!("Failed ({status}): {message} {errors:?}"),
Err(e) => return Err(e),
}
let active = zal
.subscribers(user.isp_id, user.branch_id, &[("subscriber_type", "2".to_string())])
.await?;
for s in active {
println!("{} expires {}", s["username"], s["expiration_date"]);
}
Ok(())
}Next steps
- Browse the endpoint reference — every page has the same six language tabs: Subscribers, Packages, Invoices, Payments, NAS, OLT/ONU…
- Building a subscriber-facing app instead? Use the Subscriber Auth endpoints — same pattern, 30-day tokens.
- Import the Postman collection to explore interactively.
