العودة إلى الوثائق

أمثلة التكامل

عملاء كاملون وقابلون للتشغيل بـ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.
  • ‏429 ينتظر Retry-After؛ و5xx يتراجع مع عشوائية؛ و4xx لا يُعاد.
  • الترقيم يتوقف عند صفحة قصيرة لا عند مقارنة حسابية مع total.
  • الأوقات تُحلَّل على أنها UTC.
  • ‏POST الذي تنتهي مهلته يُطابَق بقراءة الحالة لا بإعادة الإرسال.

تحتاج مساعدة تقنية؟

إن لم تجد إجابة سؤالك هنا، تواصل مع فريقنا.

تواصل معنا
أمثلة التكامل · Cliento Pro