نمونههای یکپارچهسازی
کلاینتهای کامل و قابل اجرا با 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. - ۴۲۹ منتظر
Retry-Afterمیماند؛ ۵xx با jitter عقب میکشد؛ ۴xx دوباره تلاش نمیکند. - صفحهبندی با صفحهٔ کوتاه تمام میشود، نه با مقایسهٔ حسابی با
total. - زمانها بهعنوان UTC تجزیه میشوند.
-
POSTای که تایماوت شده با بازخوانی تطبیق داده میشود، نه با ارسال دوباره.