بازگشت به مستندات

نمونه‌های یکپارچه‌سازی

کلاینت‌های کامل و قابل اجرا با cURL، TypeScript و Python - شامل صفحه‌بندی، مدیریت خطا و تلاش دوباره.

آخرین بازنگری: 2026-08-23

cURL

چرخهٔ کامل عمر یک رکورد، با توکن API از محیط. همهٔ نمونه‌های این صفحه از همین متغیر استفاده می‌کنند تا بتوان پشت سر هم اجرایشان کرد.

bash·آماده‌سازی
export CLIENTO_HOST="https://your-host"
export CLIENTO_TOKEN="ct_live_xxxxxxxxxxxxxxxxxxxxxxxx"
bash·فهرست، ایجاد، خواندن، به‌روزرسانی، حذف
# 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"
bash·دیدن بدنهٔ خطا
# -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 دارد اجرا می‌شود.

typescript·client.ts
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;
  }
}
typescript·استفاده از آن
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 را بازاستفاده می‌کند و هدر احراز هویت را با خود می‌برد؛ وقتی هزاران رکورد را صفحه‌به‌صفحه می‌پیمایید همین مهم می‌شود.

python·cliento.py
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 += 1
python·استفاده از آن
from 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ای که تایم‌اوت شده با بازخوانی تطبیق داده می‌شود، نه با ارسال دوباره.

به کمک فنی نیاز دارید؟

اگر پاسخ پرسش‌تان اینجا نبود، با تیم ما تماس بگیرید.

تماس با ما
نمونه‌های یکپارچه‌سازی · Cliento Pro