Skip to content
AISARAISAR
REST API

Contacts

A contact is the record for a person or organisation you communicate with: phone numbers, emails, linked channel accounts (contactAccounts — e.g. a specific WhatsApp number or Instagram username), tags, and a status (lead/active/inactive). A single contact can have multiple linked accounts and conversations. The API covers CRUD, lookup/resolution by identifier, duplicate merging, broadcast opt-in/opt-out management, and blocking a contact (block/unblock).

List: filters, sorting, pagination

The list supports pagination (page; perPage — 5 to 100, default 10), free-text search (search), a statuses[] filter (only active and inactive are accepted — the lead status cannot be filtered on, and passing it returns 422) and sorting (sortBy: id|display_name|company|status|created_at, default created_at; sortDir: asc|desc, default desc).

The exclude_lid_only flag (0/1, default 1) hides, by default, contacts whose only linked accounts are private WhatsApp @lid identifiers with no resolved number; pass 0 to include them.

Quick lookup

The /lookup shortcut searches phone numbers and linked accounts (external_id, username) and returns up to 20 matches.

Access and photos

A contact from another company returns 404 "Contact not found." on read, update, delete and on nested sub-resources. The photo endpoints stream the image, falling back to the linked account's profile_picture.

Creating and updating linked records

On create and update, related collections (phones, emailes, contactAccounts) are persisted as a whole: PATCH fully rebuilds the linked records from the request body.

Calling a contact

A pair of telephony endpoints (available only when telephony is enabled) answers "can I call this contact right now": GET /outbound-eligibility returns, for the given channel_id, a two-block DTO — message (the state of WhatsApp's 24-hour messaging window) and call (whether a call is allowed and whether permission is needed/can be requested) — while POST /call-permission/request sends the contact a call-permission request (WhatsApp Calling).

For SIP channels calling is always allowed (permission_status=not_required) and no permission is requested. Meta's permission-request limits: at most once per 24 hours and no more than twice per rolling 7 days — when exhausted the POST returns 429 with an eligibility field whose request_cooldown_until (Unix time when the limit lifts) is set.

Blocking a contact

POST /contacts/{contact}/block and /unblock (since 2026-09-04) perform a real block: blocked_at on the contact is set first and stays the source of truth even if every provider call below is rejected outright. The service then best-effort blocks the contact on every WhatsApp channel of the company (whatsapp and whatsapp_business) that has at least one thread with the contact — a channel the contact never messaged is left untouched. The outcome for each such channel lands in the response's provider_failures[] ({channel_id, error_code, message}); an empty array means full success (or that there were no provider channels at all), a non-empty one does not undo the local block — blocked_at in the same response is already set. Calling block again on an already-blocked contact is a no-op (message: "Contact was already blocked.", provider_failures: []), with no provider calls made; likewise for unblock on an already-unblocked contact.

GET /contacts/{contact}/block-preview answers "what would blocking do" BEFORE the block itself: channels[] lists the channels a provider-side block would reach (channel_id, channel_name, channel_type, provider_block_supported), each with a window_open flag. window_open is only meaningful for whatsapp_business: false means the contact has not written in the last 24 hours and the Cloud API will refuse the block right now (the same reason that would otherwise come back as not_reachable_24h in provider_failures[]); grey channels have no such rule and always report true. An empty channels[] means the block would be local only. Same permission (contact:update), rate-limited under throttle:api-token-heavy.

Both endpoints require the contact:update permission (like opt-out/opt-in) and share a dedicated throttle:contact-block limit: 6 changes per hour per contact, 60 per hour and 300 per day per company. To read the list of numbers the provider itself has blocked on a specific channel (not what AISAR has blocked), use GET /channels/{channel}/whatsapp/blocked-users in the "Channels" group — its authorization is different (channel:view).

Contact and user photo

GET /contacts/{contact}/photo and GET /users/{user}/photo (since 2026-08-24) always return a webp preview up to 256px wide, when the source is a decodable image wider than 256px; otherwise (already ≤256px, or GD couldn't decode the format) the original is streamed as before. There is currently no way to explicitly request the original from either route — a dedicated param would need to be added if one is ever needed. Both routes are signed and share the throttle:signed-media rate limit (2000 requests/minute per IP) with GET .../attachments/{attachment}/media.

Endpoints

MethodPath
GET/v1/contacts

List the company's contacts (query: search, statuses[]=active|inactive, sortBy, sortDir, exclude_lid_only, page, perPage).

GET/v1/contacts/lookup

Quick contact lookup by phone, external_id or channel username.

GET/v1/contacts/channel-options

List channel types available for linking an account to a contact.

GET/v1/contacts/{contact}

Fetch a single contact (phones, emailes, contactAccounts, tags).

POST/v1/contacts

Create a contact (with phones, emails and linked channel accounts).

POST/v1/contacts/resolve-identifiers

Resolve a batch of identifiers (phone/username/external_id) to existing contact ids.

PATCH/v1/contacts/{contact}

Update a contact (fully rewrites related phones/emails/accounts).

DELETE/v1/contacts/{contact}

Delete a contact (soft delete).

POST/v1/contacts/{contact}/merge

Merge a contact into another one (all conversations and accounts move to the target).

POST/v1/contacts/{contact}/tags

Sync a contact's tags (full replace of the tag set).

POST/v1/contacts/{contact}/opt-out

Opt a contact out of broadcasts.

POST/v1/contacts/{contact}/opt-in

Opt a contact back into broadcasts (clears opt-out).

GET/v1/contacts/{contact}/block-preview

What a block would reach before it is applied: the channels it would go to, and whether WhatsApp's 24-hour window is open on each.

POST/v1/contacts/{contact}/block

Block a contact (body: reason?) — sets blocked_at and best-effort blocks it on every WhatsApp channel it has messaged.

POST/v1/contacts/{contact}/unblock

Unblock a contact — clears blocked_at and best-effort unblocks it on the affected WhatsApp channels.

GET/v1/contacts/{contact}/deals

List a contact's deals (requires the deals feature to be enabled).

GET/v1/contacts/{contact}/outbound-eligibility

Check outbound-call eligibility for the contact on a channel (query: channel_id) — messaging window and call permission.

POST/v1/contacts/{contact}/call-permission/request

Request call permission from the contact (WhatsApp Calling; body: channel_id).

GET/v1/contacts/{contact}/photo

The contact's photo (falls back to the linked account's profile_picture) — a webp preview up to 256px when the source is decodable and wider; otherwise the original, streamed.

GET/v1/users/{user}/photo

A company user's photo — a webp preview up to 256px when the source is decodable and wider; otherwise the original, streamed.

Examples

Create a contact

Request

bash
curl -X POST "https://api.aisar.app/v1/contacts" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "John",
    "lastName": "Doe",
    "phones": [{ "phone": "+77001234567", "type": "mobile" }],
    "emailes": [{ "email": "john@example.com", "type": "work" }]
  }'

Response

json
{
  "data": {
    "id": 45,
    "company_id": 1,
    "firstName": "John",
    "lastName": "Doe",
    "displayName": "John Doe",
    "status": "lead",
    "phones": [{ "phone": "+77001234567", "type": "mobile" }],
    "emailes": [{ "email": "john@example.com", "type": "work" }],
    "contactAccounts": [],
    "merged_into_contact_id": null,
    "blocked_at": null,
    "blocked_reason": null,
    "blocked_by_user_id": null
  }
}

Look up a contact by phone

Request

bash
curl -X GET "https://api.aisar.app/v1/contacts/lookup?q=%2B77001234567" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": [
    {
      "id": 45,
      "displayName": "John Doe",
      "status": "lead",
      "phones": [{ "phone": "+77001234567", "type": "mobile" }]
    }
  ]
}

List contacts filtered by status

Request

bash
curl -X GET "https://api.aisar.app/v1/contacts?perPage=10&statuses=active,inactive&sortBy=created_at&sortDir=desc" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": {
    "items": [
      {
        "id": 45,
        "displayName": "John Doe",
        "status": "active",
        "phones": [{ "phone": "+77001234567", "type": "mobile" }],
        "tags": []
      }
    ],
    "pagination": { "page": 1, "perPage": 10, "total": 120, "lastPage": 12 }
  }
}

Fetch a single contact

Request

bash
curl -X GET "https://api.aisar.app/v1/contacts/45" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": {
    "id": 45,
    "company_id": 10,
    "firstName": "John",
    "lastName": "Doe",
    "displayName": "John Doe",
    "status": "lead",
    "phones": [{ "phone": "+77001234567", "type": "mobile" }],
    "emailes": [{ "email": "john@example.com", "type": "work" }],
    "contactAccounts": [
      {
        "id": 88,
        "channel_type": "whatsapp",
        "external_id": "77001234567",
        "username": null,
        "profile_picture": null
      }
    ],
    "tags": [{ "id": 3, "name": "VIP" }],
    "merged_into_contact_id": null,
    "blocked_at": null,
    "blocked_reason": null,
    "blocked_by_user_id": null
  }
}

Sync a contact's tags

Request

bash
curl -X POST "https://api.aisar.app/v1/contacts/45/tags" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tag_ids": [1, 2, 3]
  }'

Response

json
{
  "data": {
    "tags": [
      { "id": 1, "name": "Lead" },
      { "id": 2, "name": "Newsletter" },
      { "id": 3, "name": "VIP" }
    ]
  }
}

List a contact's deals

Request

bash
curl -X GET "https://api.aisar.app/v1/contacts/45/deals?perPage=20" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": {
    "items": [
      {
        "id": 7,
        "title": "Website enquiry",
        "status": "open",
        "amount": 150000,
        "funnel_id": 2,
        "stage_id": 5
      }
    ],
    "pagination": { "page": 1, "perPage": 20, "total": 3, "lastPage": 1 }
  }
}

Outbound-call eligibility (WhatsApp)

Request

bash
curl -X GET "https://api.aisar.app/v1/contacts/45/outbound-eligibility?channel_id=7" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": {
    "eligibility": {
      "message": {
        "window_open": true,
        "window_expires_at": 1710500000,
        "requires_template": false
      },
      "call": {
        "channel_kind": "whatsapp",
        "can_call": false,
        "permission_status": "none",
        "permission_expires_at": null,
        "can_request_permission": true,
        "request_cooldown_until": null,
        "attempts_remaining": 5,
        "has_whatsapp_account": true
      }
    }
  }
}

Request call permission

Request

bash
curl -X POST "https://api.aisar.app/v1/contacts/45/call-permission/request" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "channel_id": 7 }'

Response

json
{
  "data": {
    "eligibility": {
      "message": {
        "window_open": true,
        "window_expires_at": 1710500000,
        "requires_template": false
      },
      "call": {
        "channel_kind": "whatsapp",
        "can_call": false,
        "permission_status": "pending",
        "permission_expires_at": null,
        "can_request_permission": false,
        "request_cooldown_until": 1710586400,
        "attempts_remaining": 5,
        "has_whatsapp_account": true
      }
    }
  }
}

Call-permission request limit reached (429)

Request

bash
curl -X POST "https://api.aisar.app/v1/contacts/45/call-permission/request" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "channel_id": 7 }'

Response

json
{
  "data": {
    "message": "Call-permission request limit reached.",
    "eligibility": {
      "message": {
        "window_open": true,
        "window_expires_at": 1710500000,
        "requires_template": false
      },
      "call": {
        "channel_kind": "whatsapp",
        "can_call": false,
        "permission_status": "pending",
        "permission_expires_at": null,
        "can_request_permission": false,
        "request_cooldown_until": 1710586400,
        "attempts_remaining": 5,
        "has_whatsapp_account": true
      }
    }
  }
}

Block a contact

Request

bash
curl -X POST "https://api.aisar.app/v1/contacts/45/block" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Оскорбления в переписке" }'

Response

json
{
  "data": {
    "message": "Contact blocked.",
    "contact_id": 45,
    "blocked_at": "2026-09-04T10:15:00Z",
    "blocked_reason": "Оскорбления в переписке",
    "provider_failures": []
  }
}

Block a contact — the provider rejected one channel

Request

bash
curl -X POST "https://api.aisar.app/v1/contacts/45/block" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

Response

json
{
  "data": {
    "message": "Contact blocked.",
    "contact_id": 45,
    "blocked_at": "2026-09-04T10:15:00Z",
    "blocked_reason": null,
    "provider_failures": [
      { "channel_id": 9, "error_code": "channel_offline", "message": "The channel is not connected right now — connect it and try again." }
    ]
  }
}

Unblock a contact

Request

bash
curl -X POST "https://api.aisar.app/v1/contacts/45/unblock" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": {
    "message": "Contact unblocked.",
    "contact_id": 45,
    "blocked_at": null,
    "blocked_reason": null,
    "provider_failures": []
  }
}