أمثلة التكامل
عملاء كاملون وقابلون للتشغيل بـcURL وTypeScript وPython - مع الترقيم ومعالجة الأخطاء وإعادة المحاولة.
آخر مراجعة: 2026-08-23
cURL
دورة حياة سجل واحد كاملة، برمز API من البيئة. كل أمثلة هذه الصفحة تستخدم المتغيّر نفسه ليمكن لصقها بالتتابع.
export CLIENTO_HOST="https://your-host"
export CLIENTO_TOKEN="ct_live_xxxxxxxxxxxxxxxxxxxxxxxx"# List (page 1, 25 per page)
curl -sS "$CLIENTO_HOST/api/v1/crm/contacts?page=1&page_size=25" \
-H "Authorization: Bearer $CLIENTO_TOKEN"
# Create -> 201, body is the new contact
curl -sS -X POST "$CLIENTO_HOST/api/v1/crm/contacts" \
-H "Authorization: Bearer $CLIENTO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"first_name": "Sara",
"last_name": "Ahmadi",
"email": "sara@example.com",
"phone": "+989121234567",
"company": "Horizon Real Estate"
}'
# Read one
curl -sS "$CLIENTO_HOST/api/v1/crm/contacts/4182" \
-H "Authorization: Bearer $CLIENTO_TOKEN"
# Update - PATCH is partial, send only what changed
curl -sS -X PATCH "$CLIENTO_HOST/api/v1/crm/contacts/4182" \
-H "Authorization: Bearer $CLIENTO_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "title": "Managing partner" }'
# Delete -> 204, empty body
curl -sS -o /dev/null -w "%{http_code}\n" \
-X DELETE "$CLIENTO_HOST/api/v1/crm/contacts/4182" \
-H "Authorization: Bearer $CLIENTO_TOKEN"# -sS keeps curl quiet but still prints transport errors;
# -i shows the status line, which the error body does not repeat.
curl -sS -i -X POST "$CLIENTO_HOST/api/v1/crm/contacts" \
-H "Authorization: Bearer $CLIENTO_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "email": "not-an-email" }'
# HTTP/1.1 400 Bad Request
# {"error":"validation_error","message":"Invalid payload",
# "details":{"email":["Not a valid email address."]}}TypeScript / JavaScript
عميل صغير يحمل الأشياء الثلاثة التي يحتاجها كل تكامل وتُغفلها أغلب المسودات الأولى: أخطاء مُصنَّفة، وترقيم تلقائي، وإعادة محاولة تحترم Retry-After. يعمل على Node 20 فأعلى وفي أي بيئة تدعم fetch.
const HOST = process.env.CLIENTO_HOST!;
const TOKEN = process.env.CLIENTO_TOKEN!; // never hard-code this
export class ClientoError extends Error {
constructor(
readonly status: number,
readonly code: string,
message: string,
readonly details: unknown,
) {
super(message);
this.name = "ClientoError";
}
}
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export async function request<T>(
path: string,
init: RequestInit & { json?: unknown } = {},
): Promise<T> {
const headers = new Headers(init.headers);
headers.set("Authorization", `Bearer ${TOKEN}`);
headers.set("Accept", "application/json");
let body = init.body;
if (init.json !== undefined) {
headers.set("Content-Type", "application/json");
body = JSON.stringify(init.json);
}
// Up to three attempts, but ONLY for answers a retry can fix: a throttle
// (the server tells us how long to wait) and a server fault. A 400 or a 403
// is a decision, and repeating it just makes the same request twice.
for (let attempt = 0; ; attempt++) {
const res = await fetch(`${HOST}/api/v1${path}`, { ...init, body, headers });
if (res.status === 429 && attempt < 3) {
const wait = Number(res.headers.get("Retry-After") ?? 5);
await sleep(wait * 1000);
continue;
}
if (res.status >= 500 && attempt < 3) {
await sleep(2 ** attempt * 1000 + Math.random() * 500); // backoff + jitter
continue;
}
if (res.status === 204) return undefined as T;
const payload = await res.json().catch(() => null);
if (!res.ok) {
const e = payload as { error?: string; message?: string; details?: unknown } | null;
throw new ClientoError(
res.status,
e?.error ?? "unknown_error",
e?.message ?? res.statusText,
e?.details ?? null,
);
}
return payload as T;
}
}
/** Every page of a list endpoint, as one async iterator. */
export async function* paginate<T>(
path: string,
params: Record<string, string | number> = {},
): AsyncGenerator<T> {
const pageSize = Number(params.page_size ?? 100);
for (let page = 1; ; page++) {
const query = new URLSearchParams({
...Object.fromEntries(Object.entries(params).map(([k, v]) => [k, String(v)])),
page: String(page),
page_size: String(pageSize),
});
const res = await request<{ items: T[]; total: number }>(`${path}?${query}`);
for (const item of res.items) yield item;
// Stop on a short page rather than on `page * size >= total`: rows can be
// added while you walk, and the count is a moving target.
if (res.items.length < pageSize) return;
}
}import { ClientoError, paginate, request } from "./client";
type Contact = {
id: number;
display_name: string;
email: string | null;
phone: string | null;
};
// Create
const created = await request<Contact>("/crm/contacts", {
method: "POST",
json: { first_name: "Sara", last_name: "Ahmadi", email: "sara@example.com" },
});
// Update - partial
await request<Contact>(`/crm/contacts/${created.id}`, {
method: "PATCH",
json: { title: "Managing partner" },
});
// Walk every contact without holding them all in memory
for await (const contact of paginate<Contact>("/crm/contacts", { q: "ahmadi" })) {
console.log(contact.id, contact.display_name);
}
// Handle refusals by code, never by message text
try {
await request("/crm/contacts", { method: "POST", json: { email: "nope" } });
} catch (err) {
if (err instanceof ClientoError && err.code === "validation_error") {
console.error("Rejected fields:", err.details);
} else if (err instanceof ClientoError && err.status === 403) {
console.error("Token is missing:", (err.details as { required?: string[] })?.required);
} else {
throw err;
}
}Python
العميل نفسه بلغة Python باستخدام requests. جلسة Session واحدة تعيد استخدام اتصال TCP وتحمل ترويسة المصادقة، وهذا مهم حين تتنقّل عبر آلاف السجلات.
import os
import random
import time
from typing import Any, Iterator
import requests
HOST = os.environ["CLIENTO_HOST"]
TOKEN = os.environ["CLIENTO_TOKEN"] # never hard-code this
class ClientoError(Exception):
def __init__(self, status: int, code: str, message: str, details: Any):
super().__init__(f"{status} {code}: {message}")
self.status = status
self.code = code
self.details = details
session = requests.Session()
session.headers.update({
"Authorization": f"Bearer {TOKEN}",
"Accept": "application/json",
})
def request(method: str, path: str, *, json: Any = None,
params: dict | None = None, attempts: int = 3) -> Any:
"""One API call, with the retries a retry can actually fix.
429 and 5xx are retried; 4xx is a decision and is raised immediately.
"""
url = f"{HOST}/api/v1{path}"
for attempt in range(attempts):
response = session.request(method, url, json=json, params=params, timeout=30)
if response.status_code == 429 and attempt < attempts - 1:
time.sleep(int(response.headers.get("Retry-After", 5)))
continue
if response.status_code >= 500 and attempt < attempts - 1:
time.sleep(2 ** attempt + random.random()) # backoff + jitter
continue
if response.status_code == 204:
return None
try:
payload = response.json()
except ValueError:
payload = None
if not response.ok:
payload = payload or {}
raise ClientoError(
response.status_code,
payload.get("error", "unknown_error"),
payload.get("message", response.reason),
payload.get("details"),
)
return payload
raise RuntimeError("unreachable")
def paginate(path: str, *, page_size: int = 100, **filters) -> Iterator[dict]:
"""Every row of a list endpoint, one page at a time."""
page = 1
while True:
body = request("GET", path,
params={**filters, "page": page, "page_size": page_size})
items = body["items"]
yield from items
# A short page means the end. Comparing against `total` would loop
# forever if rows were added while paging.
if len(items) < page_size:
return
page += 1from cliento import ClientoError, paginate, request
# Create
contact = request("POST", "/crm/contacts", json={
"first_name": "Sara",
"last_name": "Ahmadi",
"email": "sara@example.com",
})
# Update - partial
request("PATCH", f"/crm/contacts/{contact['id']}", json={"title": "Managing partner"})
# Walk every contact
for row in paginate("/crm/contacts", q="ahmadi"):
print(row["id"], row["display_name"])
# Delete
request("DELETE", f"/crm/contacts/{contact['id']}")
# Handle refusals by code
try:
request("POST", "/crm/contacts", json={"email": "nope"})
except ClientoError as err:
if err.code == "validation_error":
print("Rejected fields:", err.details)
elif err.status == 403:
print("Token is missing:", (err.details or {}).get("required"))
else:
raiseقبل الإطلاق
- الرمز يأتي من البيئة أو من مدير أسرار - لا من المستودع.
- حقول الاستجابة غير المعروفة تُتجاهل، فلا يكسرك تغيير إضافي في الـ API.
- معالجة الأخطاء تتفرّع على
errorوstatus، لا علىmessage. - 429 ينتظر
Retry-After؛ و5xx يتراجع مع عشوائية؛ و4xx لا يُعاد. - الترقيم يتوقف عند صفحة قصيرة لا عند مقارنة حسابية مع
total. - الأوقات تُحلَّل على أنها UTC.
-
POSTالذي تنتهي مهلته يُطابَق بقراءة الحالة لا بإعادة الإرسال.