Back to documentation

API overview

Base URL, versioning, request and response shapes, and the tenancy model every call is evaluated against.

Last revised: 2026-08-23

What the API is for

The Cliento Pro API is the same interface the product's own web application uses. There is no reduced 'public' subset and no separate integration database: if a screen can show it, an authorised token can read it, and if a screen can change it, an authorised token can change it.

Use it to synchronise contacts with another system, push orders in from a storefront, pull deals into a warehouse for reporting, or drive any workflow the UI drives. For the opposite direction - being told when something changes, rather than asking - use webhooks.

Base URL and versioning

Every resource lives under a single versioned root. The version is in the path, and it is the only place a version appears - there is no version header and no Accept-parameter negotiation.

http·The versioned root
https://<your-cliento-host>/api/v1

/api/v1 changes additively. New endpoints, new optional request fields and new response fields may appear at any time, so a client must ignore fields it does not recognise rather than failing on them. A breaking change means a new version path and a deprecation period on the old one - it never arrives inside v1.

Making a request

  • HTTPS only. Tokens are bearer credentials; a plaintext request leaks one.
  • `Authorization: Bearer <token>` on every endpoint except sign-up and login. See Authentication.
  • `Content-Type: application/json` on any request with a body. Bodies are JSON objects - never form encoding, never a bare array at the top level.
  • `Accept: application/json`. Responses are JSON; errors are JSON too, including 401 and 500.
  • UTF-8 throughout. Persian, Arabic and emoji are stored and returned unchanged.
bash·A complete request
curl -sS https://your-host/api/v1/crm/contacts \
  -H "Authorization: Bearer ct_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Accept: application/json"

Response shapes

Three shapes cover almost every endpoint, and knowing which one you are looking at is enough to write a client.

CallStatusBody
List (GET /crm/contacts)200{ items, page, page_size, total }
Detail (GET /crm/contacts/{id})200The record object
Create (POST /crm/contacts)201The created record
Update (PATCH /crm/contacts/{id})200The updated record
Delete (DELETE /crm/contacts/{id})204Empty
Anything refused4xx / 5xx{ error, message, details }
json·A detail response
{
  "id": 4182,
  "tenant_id": 17,
  "first_name": "Sara",
  "last_name": "Ahmadi",
  "email": "sara@example.com",
  "phone": "+989121234567",
  "company": "Horizon Real Estate",
  "title": "Managing partner",
  "owner_user_id": 5,
  "created_by_user_id": 5,
  "display_name": "Sara Ahmadi",
  "created_at": "2026-08-01T09:14:22",
  "updated_at": "2026-08-19T16:02:41"
}

PATCH is genuinely partial: send only the fields you are changing. Sending a field with null sets it to null; omitting it leaves it alone. There is no PUT, because a full-replacement write would silently clear fields added to the product after your client was written.

Workspaces, permissions and visibility

Every credential belongs to exactly one workspace, and the workspace is read from the credential - never from the request. There is no tenant parameter you can send, which is the point: a token cannot be pointed at another customer's data by changing a URL, a header or a body field.

Three checks run on every call, in order. Failing any of them is a refusal, not an empty result:

  1. Authentication - the token is live, unexpired and not revoked. Otherwise 401.
  2. Permission - the token carries the specific code the endpoint requires, e.g. crm.contacts.write for creating a contact. Otherwise 403.
  3. Row visibility - within the workspace, some roles see only records they own or their team owns. Rows outside that scope are simply absent from lists and 404 on direct access.

Individual *fields* can be restricted too. A caller without crm.contacts.view_phone receives phone masked - the same length, every character replaced - rather than removed. Render the masked value; do not treat it as an empty field.

Everything you do is recorded

Every state change writes an event, whether it came from the UI, a workflow or your integration. GET /events is that log, and it is also what webhooks are built on - so an integration can reconcile against the same history the workspace's own audit view shows.

json·One event
{
  "id": 91442,
  "tenant_id": 17,
  "user_id": 5,
  "entity_type": "contact",
  "entity_type_label": { "en": "Contact", "fa": "مخاطب", "ar": "جهة اتصال" },
  "entity_id": 4182,
  "event_type": "ContactCreated",
  "event_type_label": { "en": "Contact created", "fa": "مخاطب ایجاد شد", "ar": "تم إنشاء جهة الاتصال" },
  "source": "api",
  "metadata": { "email": "sara@example.com" },
  "occurred_at": "2026-08-19T16:02:41"
}

event_type and entity_type are stable machine codes - match on those. The *_label pairs carry the product's own wording in all three locales and are for display only; they may be reworded without notice.

Language and calendars

The API itself is language-neutral, and that is a deliberate contract rather than an omission:

  • Timestamps are UTC, ISO-8601, without an offset suffix (2026-08-19T16:02:41). Jalali and Gregorian rendering is a display choice each user makes; the wire format never changes.
  • Enumerations are machine codes (open, in_progress, ContactCreated). Translate them in your own client - do not match on a display label.
  • Error messages are English. They are diagnostic text for a developer. Localize by error code, which is stable, not by the sentence.
  • Content you store is returned verbatim, in whatever script you sent it.

Need technical help?

If your question is not answered here, get in touch with our team.

Contact us
API overview · Cliento Pro