Skip to content
AISARAISAR
REST API

Teams & Users

Manage teams (operator grouping + channel access), company members, and email invitations for new users. The group also includes the current-user block (GET /me): the profile with roles and permissions in the active company, availability status, and switching the active company. GET /v1/users lists the company's real (non-service) users. A separate block covers provisioning SIP telephony for operators (companies/current/members/telephony*, admin-gated by company_telephony.manage) and operator self-service (/v1/softphone/*, session/mobile token only). GET /v1/bootstrap bundles the cabinet's start-up reads (profile, subscription, configs, unread counters) into a single request instead of nine.

Roles and membership

Roles are assigned at the company level (admin/manager/agent) via Spatie permissions in teams mode — one user can belong to multiple companies with different roles in each.

POST /v1/companies/current/members/{memberId}/role and .../revoke work on people only: an API token's service account (see the "API Tokens" group) cannot be demoted or revoked through them — it gets 404 with the same "User not found." body as a non-existent user. The only way to switch an integration off is to delete its API token (DELETE /v1/api-tokens/{id}). A role change sends the user.role_changed webhook with source: "company" — only when the role actually changes: re-assigning the same role emits no event.

Teams

A team groups operators together and opens them access to a set of channels (see "Channel access rules" in the "Channels" group). When team access restrictions are turned on for the platform (see there too), changing a team's MEMBERSHIP (member_ids) or its CHANNELS (channel_access_mode/channel_ids) via POST/PATCH /v1/teams is limited to the company admin or platform staff — otherwise 403 {"message": "...", "code": "channel_access_admin_only"}, and the whole request is rolled back. The decision is made on the request's actual EFFECT, not on which keys are present: renaming a team (name/description) without changing its membership or channels succeeds for any user holding team.update, even if the request body resends the same channel_ids — the cabinet form typically always sends them, and that is not treated as an attempt to change them unless the resulting list actually differs from the current one.

Saving a team (channel_ids) accepts both live and archived company channels — a team's rule on an archived channel is not lost by an ordinary save (a rename, say, where the form resends the whole channel list), even though access to an archived channel is not normally edited directly through the UI. Separately, a channel awaiting final deletion (DELETE /channels/archived/{channelId} has already queued it) is NOT accepted as a new channel_ids value and is not removed from an already-saved team rule set either — it is simply excluded from the check: that rule disappears on its own, together with the channel, once the deletion queue finishes.

DELETE /v1/teams/{team} follows the same rule too: deleting a team drops its channel-access rules, so it requires an admin/platform staff only when the team actually carries at least one access rule (channel_access) — a team with no rule at all can be deleted by any holder of team.delete, restrictions on or off. Separately from team permissions: a former employee is automatically removed from EVERY team of the company the moment they lose their role in it (offboarding, a revoked invitation, leaving) — keeping a team's member list current by hand is not necessary.

Invitations

An invitation lives for 30 days (expires_at). Its status is one of pending, accepted, declined, expired and revoked, and the status you receive always accounts for the deadline: an invitation whose expires_at has passed comes back as expired, never as pending. A background sweep (every 15 minutes) moves the record to the terminal status, but the response does not depend on it — an overdue invitation reads expired even before the sweep has run. So there is no need to compare expires_at against the current time yourself: a pending status in the response means the invitation really can still be accepted.

The same value arrives as the invitation_status field on every row of GET /v1/companies/current/members: accepted for a member who has already joined, and the deadline-aware status for an invitation row.

If POST /v1/invitations is called with the email of a user who already belongs to the company, the request is refused with 422 and errors.email, error: "member_already_exists" — no invitation is created and no seat is spent from the user limit.

Re-inviting the same email while the earlier invitation is still active (pending, not expired) does not create a second record: the same record is refreshed instead — a new guid (the previous link stops working), the new role and a new expiry. The user-limit check is skipped in this case — refreshing a live invitation does not spend a seat.

A race between two simultaneous invitation requests for the same email in the same company never produces a duplicate or a 500: the losing request gets the same outcome as the re-invite case above — the same invitation, refreshed with a new guid and the requested role and expiry.

The public, unauthenticated invitation-resolution steps (resolve/context/intent) are out of scope for this reference — see the invitations guide.

Current user

The current-user block (GET /me) returns the profile together with the caller's roles and permissions in the active company — the starting point for service-token consumers; companion endpoints update the profile, availability status, and active company (switching company returns the profile with the new company's roles and permissions).

Since 2026-09-26 channel_ids in GET /me lists only the company's (non-draft) channels the user can access under channel_access rules; an admin still gets every channel.

Since 2026-09-30, under an embed token (bound to a single company by the company:{id} ability — see the "API Tokens" group and the partner integration docs) GET /me is narrowed to that company: memberships contains exactly one entry, the bound company; pending_invitations is always [] (its guid is a bearer secret for joining a DIFFERENT company); has_partner_access is always false. There is no reason to disclose to a partner origin which other companies the operator belongs to — the token cannot act anywhere but the bound company anyway. For a cabinet session, a mobile token, and ordinary (*-ability) service tokens the response is unchanged.

Since 2026-09-30 GET /me (and the profile in the PUT /v1/me/current-company response — the same shape) carries a boolean can_edit_channel_access: whether the CURRENT user may right now change channel access rules (.../access) and a team's member list/channel set in the active company (see "Channel access rules" in the "Channels" group and "Teams" below). While team access restrictions are off for the platform (the current default), the field is always true; once turned on, it is true only for the company admin and platform staff, false for everyone else. The field does not reserve or guarantee anything by itself: the ordinary permissions (channel.update for channel access, team.* for teams) are still required and checked independently — the cabinet combines can_edit_channel_access with them to decide whether to show the relevant controls, not in place of them.

The availability status (PUT /v1/me/availability) accepts exactly three writable values: online, away, offline (busy is a system-derived state and cannot be set through this endpoint).

Since 2026-09-23 photo in POST /v1/me is no longer a free string: the value must be either an absolute http(s) URL or a {current_company_id}/users/... path — anything else is rejected with 422. The signed photo link this same GET /v1/me returned can be sent back unchanged — it is read as "the photo was not touched" and simply not re-saved.

Active company

The active company is stored in a single server-side field, current_company_id, on your account: it is shared across all of your sessions and API tokens (it is not scoped to an individual token), and company-scoped lists (for example GET /v1/conversations) and realtime channel authorization follow it.

So if the same user switches company in another tab, app, or token, every context follows — an embedded chat may suddenly show another company's conversations, and subscriptions to the previous (no-longer-active) company's channels start returning 403. Keep the company you read from and subscribe to in sync with current_company_id.

Notification preferences

GET /v1/me/notification-preferences returns a snapshot with four blocks: channels — boolean master switches per delivery channel (mail, push, whatsapp; a missing key defaults to true), categories — an object per notification category with enabled/label/controllable fields, plus push_triggers and quiet_hours (see below). The transactional category (verification codes, invitations, password resets) is always controllable: false and enabled: true — it cannot be turned off; the remaining categories (onboarding, trial, billing, win_back, engagement) can each be toggled independently.

PUT /v1/me/notification-preferences — a partial update (merge, not replace): send only the channels.*/categories.*/push_triggers.*/quiet_hours.* keys you are changing — every other key, including the other blocks entirely, keeps its current value. channels.whatsapp = false is the only way for a user to opt out of the WhatsApp channel for billing notifications (payment reminders, suspension notices, etc.) — this opt-out is required by Meta's policy for WhatsApp templates and must be honored by the sender.

Both endpoints also carry push_triggers — a per-push-type object of boolean switches: chat_message_new, conversation_assigned, call_missed, call_incoming, company_unassigned. All default to enabled except company_unassigned (a push about a conversation in the company having no assignee — opt-in so it does not flood every operator at once). call_incoming is effectively always delivered regardless of its stored value — an incoming call is a system notification that cannot be suppressed.

quiet_hours is a do-not-disturb window: { "enabled": bool, "start": "HH:MM"|null, "end": "HH:MM"|null }, defaulting to enabled: false with start/end as null. While the window is active, non-critical push notifications (e.g. conversation_assigned) are suppressed — except call_incoming, which quiet hours never affect. The time is interpreted in the user's timezone (users.timezone); if start is greater than end, the window is treated as overnight (wraps past midnight, e.g. 22:00–08:00).

Phone number verification

The profile phone number (users.phone) can be verified with a code delivered over WhatsApp: POST /v1/me/phone/send-otp sends a 6-digit code and POST /v1/me/phone/verify with a {"code": "123456"} body checks it. A verified number is reflected by the phone_verified field in GET /v1/me and is the address used for WhatsApp copies of billing notifications. Changing the number via POST /v1/me clears the verification.

send-otp does not accept a number in the request body — the code always goes to the saved profile number, so save the number first and request the code afterwards. A 200 response carries the masked phone and expires_in (code lifetime in seconds, currently 600). Possible errors: 422 — no number set, or it fails the E.164 format check; 429 — send/verify rate limits exceeded; 409 (verify only) — the number changed between sending and verifying, request a new code; 502 — the message could not be delivered; 503 — the platform-wide daily code-sending budget is exhausted.

Both endpoints are available to session authentication and mobile tokens only. Service accounts, client API tokens, and embed tokens receive 403 — verifying a number stays an action performed by a real user.

Operator telephony

Provisioning SIP telephony for an operator is the step that follows connecting a SIP channel (see the channel-connection guide, "SIP telephony (BYO trunk)" section). A company admin (the company_telephony.manage permission) enables telephony for a specific member via POST /v1/companies/current/members/{memberId}/telephony/enable, which provisions TWO SIP profiles at once: a browser profile agent-{id}-web (for the built-in WebRTC softphone in the AISAR apps, opaque password) and a native profile agent-{id} (for desk SIP phones and apps like Zoiper/MicroSIP/Linphone, typeable password). There is no separate toggle for the native profile — disable removes both profiles together.

The admin endpoints (companies/current/members/telephony*) are called with the company api_token, same as the rest of this group — a partner service account with the admin role (which unconditionally carries company_telephony.manage, see below) can provision operators programmatically. Self-service (/v1/softphone/*) is the opposite: those endpoints require the caller to BE that operator (session authentication or a mobile token — the same pattern as phone verification above), so a company api_token / service account gets 403 here — a service account never has a SIP profile of its own (member provisioning deliberately filters is_service_account = false). Partners also have a server-to-server way to enable telephony for operators without a user session — it is covered in the partner integration documentation.

company_telephony.manage is not a plan gate: since 2026-08-13, the admin role in ANY company unconditionally carries this permission (a backfill migration closed the gap for companies provisioned before the permission existed). The plan gate lives separately — the plan's telephony feature is required for an operator to actually USE calling (TelephonyAccess::availableTo), but not for an admin to manage operators, schemes and channels.

Both ways of reading the native profile's credentials — the admin's GET .../telephony/credentials and the operator's own GET /v1/softphone/external-credentials — go through the same builder (ExternalSipCredentialPresenter), so the response shape cannot drift apart: sip_username, sip_password, host, tcp_port (5060), tls_port (5061), default_transport: "TCP", codecs: ["opus", "PCMU", "PCMA"], media_encryption, registered (whether the SIP registration is live right now — a best-effort ARA check, treated as false on error) and provisioning_uri — a generic sip: URI (sip:user:pass@host:port;transport=tcp) for QR import into Zoiper / MicroSIP / Linphone / Grandstream Wave. Not usable with 3CX — its app only reads its own provisioning-QR format, so those fields must be entered by hand there. The browser (agent-{id}-web) password is opaque: the admin GET .../telephony/credentials never returns it — that endpoint only touches the native profile; POST .../telephony/reset-password (admin), by contrast, DOES return it — the response carries the freshly rotated browser profile's sip_username/sip_password (the same one-time-reveal principle as creating an API token), and silently rotates the native profile too without returning its new password in the same response — the operator sees their new native password via their own /softphone/external-credentials.

GET /v1/companies/current/members/telephony returns one row per real (non-service) company member: user_id, name, email, photo, telephony_enabled (whether the browser profile is active), browser_registered, extension, external_enabled (whether the native profile is active) and external_registered. The *_registered pair is a live SIP registration right now, reported separately for the browser softphone (browser_registered) and for the desk phone / external SIP app (external_registered); both flags come from a single batched ARA lookup, and if ARA is unreachable both degrade to false — the operators list itself never fails because of it. They are plain "right now or not" booleans: no registration history is kept, so "offline since 09:40" cannot be derived from the response. The embedded softphone profile is deliberately left out of these flags — it is a softphone inside a partner panel iframe, not a device belonging to the company's own operator.

Since 2026-10-20 every row of GET /v1/companies/current/members/telephony carries an outbound block, describing which number the employee calls from: { "schemes": [ { "id", "name", "via", "channel_ids" } ], "primary_scheme_id": string|null, "primary_source": string|null, "falls_back_to": string|null }. schemes are the outbound schemes the employee belongs to (via says personally or through a team; channel_ids are the scheme's lines); primary_scheme_id and primary_source say which scheme is primary and how that was decided: explicit (chosen by an admin), implicit (picked automatically by the old order: personal before team) or single (the only scheme); null when there are no schemes; falls_back_to is filled only for an employee with no schemes: company_default (the shared number), legacy (any available line) or company_default_unavailable (a shared number is set but unavailable, so the employee's calls are refused). The .../telephony* endpoints of this group are closed to embed tokens: the list now names schemes.

PUT /v1/companies/current/members/{memberId}/telephony/outbound-primary makes one of an employee's schemes their primary one: body { "scheme_id": "uuid" | null } (the field is required; null clears the explicit choice and returns to the automatic one). The scheme must be outbound, active and your company's, and the employee must already belong to it; the endpoint never adds anybody to a scheme. The response is the employee's row in the same shape as the list above (with the refreshed outbound block). 422 refusals: scheme_not_found (a foreign, draft or missing scheme, indistinguishable) and not_a_member_of_scheme. POST /v1/companies/current/members/telephony/outbound-primary/bulk makes one scheme primary for many at once: body { "user_ids": [int, ...], "scheme_id": "uuid" } (1 to 500 ids; target is optional and only allows scheme; there is no bulk "own direct number" action). Response: { "data": { "updated": [user_id, ...], "skipped": [ { "user_id", "reason": "not_in_scheme" } ] } }. A single id of an outsider rejects the whole request (422) and nothing is written. Both endpoints require company_telephony.manage and a full-access token, are closed to embed tokens, are rate-limited by telephony-outbound-primary, and write an audit record (telephony.outbound_primary.set / .bulk_set) and the telephony.outbound.updated WebSocket event.

POST .../telephony/extension — the operator's internal extension, 3–4 digits (^\d{3,4}$), unique within the company; a blank value clears it. Requires telephony to already be enabled — otherwise 422.

Operator self-service (session/mobile token only): GET /v1/softphone/credentials — the operator's own browser credentials (consumed directly by the built-in WebRTC softphone, not meant for manual entry); POST /v1/softphone/reset-password — rotates them; GET /v1/softphone/external-credentials / POST /v1/softphone/external/reset-password — the same for the native profile, including the provisioning_uri for QR import; GET /v1/softphone/colleagues — telephony-enabled colleagues in the company (dialer directory) with extension and availability; POST /v1/softphone/internal-call — call a colleague by callee_user_id or internal extension; POST /v1/softphone/external-call — an outbound call over the company's SIP trunk to an external number, with an optional contact_id (resolved strictly within the company — a foreign/unknown id is 422, no silent fallback to number-based resolution). external-call shares the call-originate rate limit with POST /v1/calls/originate — 20 requests per minute per user; the same sip_outbound_gated behavior and IP-mode anti-abuse check as /calls/originate also apply here (see the Calls group).

Cabinet shell bootstrap

GET /v1/bootstrap is one request instead of the nine separate ones the cabinet (my.aisar.app) makes on load. The parts: me (GET /v1/me), subscription (GET /v1/subscription), usage (GET /v1/subscription/usage), plans (GET /v1/plans), subscription_config (GET /v1/subscription/config), bank_transfer_config (GET /v1/billing/bank-transfer/config), nudges (GET /v1/nudges), ai_pending_actions (GET /v1/ai-pending-actions), chat_behavior (GET /v1/companies/current/chat-behavior), notifications_unread_count (GET /v1/notifications/unread-count) and tasks_counters (GET /v1/deal-tasks/counters, since 2026-09-26; null plus the refusal status for a company without the deals feature). All eleven parts are returned by default; ?parts=a,b,c requests a subset — an unknown part name or a repeated one in the list is refused with 422.

Each part is built by the very same controller, FormRequest and route-level middleware as its stand-alone endpoint — the part's shape and access rules are never restated and cannot drift from the source (the same subscription gate, the same company-type gate, the same permissions). Response shape: { "data": { "parts": {...}, "status": {...}, "errors": {...} } }. parts.<name> is the part's body, unwrapped exactly the way the cabinet's api client unwraps it (the response's data if present, otherwise the whole body), or null if the part failed. status.<name> is the HTTP status the stand-alone endpoint would have answered with. errors.<name> is present only for 4xx (the same JSON body the stand-alone endpoint would have given — a rights refusal, a company-type refusal, a subscription refusal, etc.); on 5xx there is no errors entry, parts.<name> is still null, and the failure is logged server-side — the client should call that part's stand-alone endpoint directly in that case. One failing part never fails the rest.

Access is limited to a cabinet session (cookie) or a token with the * ability (service accounts, including a plain company api_token, and the mobile app). A token confined to a path allowlist (embed:*, connect-session:*) gets 403 regardless of what its own allowlist would permit per part — the embedded chat/softphone and the channel-connect wizard have no business requesting the cabinet's whole start-up bundle. No authentication — 401. A dedicated shell-bootstrap limiter caps it at 30 requests per minute per user (session/cookie calls are not covered by the general bearer-token limit, and one call builds ten endpoints' worth of work).

Endpoints

MethodPath
GET/v1/teams

List teams

POST/v1/teams

Create a team

GET/v1/teams/{team}

Get a team

PATCH/v1/teams/{team}

Update a team

DELETE/v1/teams/{team}

Delete a team

GET/v1/companies/current/members

List current company members

POST/v1/companies/current/members/{memberId}/role

Change a member's role

POST/v1/companies/current/members/{memberId}/revoke

Revoke a member's access

GET/v1/users

List current company users (excluding service accounts)

POST/v1/invitations

Send an invitation (email + role); at most 6 per minute per user, 429 beyond that

POST/v1/invitations/{guid}/accept

Accept an invitation

POST/v1/invitations/{guid}/decline

Decline an invitation

POST/v1/invitations/{invitationId}/role

Change the role on a pending invitation

POST/v1/invitations/{invitationId}/revoke

Revoke an invitation

DELETE/v1/invitations/{invitationId}

Delete an invitation

GET/v1/bootstrap

Cabinet shell in one request: me, subscription, usage, plans, subscription_config, bank_transfer_config, nudges, ai_pending_actions, chat_behavior, notifications_unread_count. ?parts=a,b — a subset; an unknown/duplicated part — 422. Cabinet session or a * ability token only; 30/min limit.

GET/v1/me

Current user profile (roles and permissions)

POST/v1/me

Update the profile

POST/v1/me/password

Change the password

PUT/v1/me/availability

Set availability status (online, away, offline)

PUT/v1/me/current-company

Switch the active company (single field shared by all sessions/tokens)

GET/v1/me/notification-preferences

Notification preference snapshot: channels (mail/push/whatsapp) + categories + push triggers (push_triggers) + quiet hours (quiet_hours)

PUT/v1/me/notification-preferences

Partially update preferences (channels.mail/push/whatsapp, categories.*, push_triggers.*, quiet_hours)

POST/v1/me/phone/send-otp

Send a verification code over WhatsApp to the saved profile number

POST/v1/me/phone/verify

Check the code and mark the profile number verified

GET/v1/companies/current/members/telephony

List the company's real members with their telephony status (enabled, extension, whether the browser softphone and the desk phone are registered right now) — for the operators admin table.

POST/v1/companies/current/members/{memberId}/telephony/enable

Enable telephony for a member: provisions the browser (agent-{id}-web) and native (agent-{id}) SIP profiles at the same time.

POST/v1/companies/current/members/{memberId}/telephony/disable

Disable telephony for a member: removes both SIP profiles.

GET/v1/companies/current/members/{memberId}/telephony/credentials

A member's native SIP profile credentials for an admin (without logging in as them) — host/ports/codecs/provisioning_uri; the browser password is not returned.

POST/v1/companies/current/members/{memberId}/telephony/reset-password

Rotate a member's SIP passwords: returns the new browser-profile password and silently rotates the native one too.

POST/v1/companies/current/members/{memberId}/telephony/extension

Assign/clear a member's internal extension (3–4 digits, unique per company); requires telephony to be enabled.

PUT/v1/companies/current/members/{memberId}/telephony/outbound-primary

Make an outbound scheme the primary one for an employee (`scheme_id`, or `null` for the automatic choice); the employee must already belong to the scheme.

POST/v1/companies/current/members/telephony/outbound-primary/bulk

Make one outbound scheme primary for a list of employees (up to 500); those not in the scheme are skipped with `not_in_scheme`.

GET/v1/softphone/credentials

The calling operator's own browser SIP profile credentials. Requires a session/mobile token — a company api_token gets 403.

POST/v1/softphone/reset-password

Rotate the operator's own browser SIP password.

GET/v1/softphone/external-credentials

The operator's own native SIP profile credentials (for a desk phone/Zoiper/MicroSIP) with a provisioning_uri for QR import.

POST/v1/softphone/external/reset-password

Rotate the operator's own native SIP password; also rotates the browser one.

GET/v1/softphone/colleagues

Telephony-enabled colleagues in the company (dialer directory) with extension and availability.

POST/v1/softphone/internal-call

An internal call to a colleague by callee_user_id or internal extension.

POST/v1/softphone/external-call

An outbound call to an external number over the company's SIP trunk; optional contact_id (strictly company-scoped); shares the call-originate limit — 20/min.

Examples

Cabinet start-up bootstrap (subset of parts)

Request

bash
curl "https://api.aisar.app/v1/bootstrap?parts=subscription,chat_behavior" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": {
    "parts": {
      "subscription": {
        "id": 42,
        "plan": "business",
        "status": "active"
      },
      "chat_behavior": null
    },
    "status": {
      "subscription": 200,
      "chat_behavior": 403
    },
    "errors": {
      "chat_behavior": {
        "message": "This action is unauthorized."
      }
    }
  }
}

Invite a new user

Request

bash
curl -X POST https://api.aisar.app/v1/invitations \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "new.agent@example.com",
    "role": "agent"
  }'

Response

json
{
  "data": {
    "message": "Invitation sent.",
    "invitation": {
      "id": 77,
      "guid": "b6e2a4d0-1234-4a90-9e21-5d3f0c8a9b11",
      "invite_url": "https://my.aisar.app/invitation/b6e2a4d0-1234-4a90-9e21-5d3f0c8a9b11",
      "email": "new.agent@example.com",
      "role": "agent",
      "expires_at": "2026-03-22T10:00:00.000000Z"
    }
  }
}

Create a team

Request

bash
curl -X POST https://api.aisar.app/v1/teams \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Support",
    "description": "Customer success team",
    "channel_access_mode": "custom",
    "channel_ids": [7, 9],
    "member_ids": [5, 6]
  }'

Response

json
{
  "data": {
    "message": "Team created.",
    "team": {
      "id": 3,
      "company_id": 1,
      "name": "Support",
      "description": "Customer success team",
      "channel_access": { "mode": "custom", "channel_ids": [7, 9] },
      "member_ids": [5, 6],
      "created_at": "2026-03-15T10:00:00.000000Z",
      "updated_at": "2026-03-15T10:00:00.000000Z"
    }
  }
}

Only an admin may change team membership/channels (403)

Request

bash
curl -X PATCH https://api.aisar.app/v1/teams/3 \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "member_ids": [5, 6, 9]
  }'

Response

json
{
  "data": {
    "message": "Only a company admin can change channel access or team membership while team access rules are enforced.",
    "code": "channel_access_admin_only"
  }
}

Current user profile and permissions

Request

bash
curl https://api.aisar.app/v1/me \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": {
    "id": 12,
    "name": "Aisha",
    "lastname": "Nurlanova",
    "email": "aisha@example.com",
    "email_verified": true,
    "phone": "+12025550102",
    "phone_verified": true,
    "photo": null,
    "country_code": "KZ",
    "language": "ru",
    "timezone": "Asia/Almaty",
    "current_company_id": 3,
    "roles": ["admin"],
    "permissions": ["company.view", "conversation.view", "message.send"],
    "company": { "id": 3, "name": "Acme" },
    "memberships": [
      { "company_id": 3, "company_name": "Acme", "roles": ["admin"], "status": "active" }
    ],
    "can_edit_channel_access": true,
    "pending_invitations": []
  }
}

Update the profile

Request

bash
curl -X POST https://api.aisar.app/v1/me \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Aisha",
    "lastname": "Nurlanova",
    "phone": "+12025550102",
    "country_code": "KZ",
    "language": "ru",
    "timezone": "Asia/Almaty"
  }'

Response

json
{
  "data": {
    "id": 12,
    "name": "Aisha",
    "lastname": "Nurlanova",
    "email": "aisha@example.com",
    "phone": "+12025550102",
    "country_code": "KZ",
    "language": "ru",
    "timezone": "Asia/Almaty",
    "current_company_id": 3
  }
}

Change the password

Request

bash
curl -X POST https://api.aisar.app/v1/me/password \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "current_password": "YOUR_CURRENT_PASSWORD",
    "password": "YOUR_NEW_PASSWORD",
    "password_confirmation": "YOUR_NEW_PASSWORD"
  }'

Response

json
{
  "data": {
    "message": "Password updated."
  }
}

Update availability status

Request

bash
curl -X PUT https://api.aisar.app/v1/me/availability \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "availability": "away"
  }'

Response

json
{
  "status": "ok"
}

Switch the active company

Request

bash
curl -X PUT https://api.aisar.app/v1/me/current-company \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "company_id": 4
  }'

Response

json
{
  "data": {
    "id": 12,
    "current_company_id": 4,
    "roles": ["manager"],
    "permissions": ["company.view", "conversation.view"]
  }
}

Company users

Request

bash
curl https://api.aisar.app/v1/users \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": [
    {
      "id": 12,
      "name": "Aisha",
      "lastname": "Nurlanova",
      "email": "aisha@example.com",
      "availability": "online",
      "last_login_at": "2026-03-20T09:14:00.000000Z"
    },
    {
      "id": 13,
      "name": "Damir",
      "lastname": "Serik",
      "email": "damir@example.com",
      "availability": "away",
      "last_login_at": "2026-03-19T18:02:00.000000Z"
    }
  ]
}

Notification preference snapshot

Request

bash
curl https://api.aisar.app/v1/me/notification-preferences \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": {
    "channels": {
      "mail": true,
      "push": true,
      "whatsapp": true
    },
    "categories": {
      "onboarding": { "enabled": true, "label": "Onboarding", "controllable": true },
      "trial": { "enabled": true, "label": "Trial", "controllable": true },
      "billing": { "enabled": true, "label": "Billing", "controllable": true },
      "win_back": { "enabled": true, "label": "Win-back", "controllable": true },
      "engagement": { "enabled": false, "label": "Engagement", "controllable": true },
      "transactional": { "enabled": true, "label": "Transactional", "controllable": false }
    },
    "push_triggers": {
      "chat_message_new": true,
      "conversation_assigned": true,
      "call_missed": true,
      "call_incoming": true,
      "company_unassigned": false
    },
    "quiet_hours": {
      "enabled": false,
      "start": null,
      "end": null
    }
  }
}

Opt out of the WhatsApp billing-notification channel

Request

bash
curl -X PUT https://api.aisar.app/v1/me/notification-preferences \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channels": {
      "whatsapp": false
    }
  }'

Response

json
{
  "data": {
    "channels": {
      "mail": true,
      "push": true,
      "whatsapp": false
    },
    "categories": {
      "onboarding": { "enabled": true, "label": "Onboarding", "controllable": true },
      "trial": { "enabled": true, "label": "Trial", "controllable": true },
      "billing": { "enabled": true, "label": "Billing", "controllable": true },
      "win_back": { "enabled": true, "label": "Win-back", "controllable": true },
      "engagement": { "enabled": false, "label": "Engagement", "controllable": true },
      "transactional": { "enabled": true, "label": "Transactional", "controllable": false }
    },
    "push_triggers": {
      "chat_message_new": true,
      "conversation_assigned": true,
      "call_missed": true,
      "call_incoming": true,
      "company_unassigned": false
    },
    "quiet_hours": {
      "enabled": false,
      "start": null,
      "end": null
    }
  }
}

Quiet hours and the unassigned-conversation push

Request

bash
curl -X PUT https://api.aisar.app/v1/me/notification-preferences \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "push_triggers": {
      "company_unassigned": true
    },
    "quiet_hours": {
      "enabled": true,
      "start": "22:00",
      "end": "08:00"
    }
  }'

Response

json
{
  "data": {
    "channels": {
      "mail": true,
      "push": true,
      "whatsapp": true
    },
    "categories": {
      "onboarding": { "enabled": true, "label": "Onboarding", "controllable": true },
      "trial": { "enabled": true, "label": "Trial", "controllable": true },
      "billing": { "enabled": true, "label": "Billing", "controllable": true },
      "win_back": { "enabled": true, "label": "Win-back", "controllable": true },
      "engagement": { "enabled": false, "label": "Engagement", "controllable": true },
      "transactional": { "enabled": true, "label": "Transactional", "controllable": false }
    },
    "push_triggers": {
      "chat_message_new": true,
      "conversation_assigned": true,
      "call_missed": true,
      "call_incoming": true,
      "company_unassigned": true
    },
    "quiet_hours": {
      "enabled": true,
      "start": "22:00",
      "end": "08:00"
    }
  }
}

List company operators

Request

bash
curl https://api.aisar.app/v1/companies/current/members/telephony \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": [
    {
      "user_id": 12,
      "name": "Aisha Nurlanova",
      "email": "aisha@example.com",
      "photo": null,
      "telephony_enabled": true,
      "browser_registered": true,
      "extension": "101",
      "external_enabled": true,
      "external_registered": true,
      "outbound": {
        "schemes": [
          { "id": "0199a1e2-7c3a-7f4e-9c2b-6a1d8e7b1c31", "name": "Продажи", "via": "personal", "channel_ids": [412] },
          { "id": "0199a1e2-8d4b-7f4e-9c2b-6a1d8e7b1c99", "name": "Вечерние обзвоны", "via": "team", "channel_ids": [413] }
        ],
        "primary_scheme_id": "0199a1e2-7c3a-7f4e-9c2b-6a1d8e7b1c31",
        "primary_source": "explicit",
        "falls_back_to": null
      }
    },
    {
      "user_id": 13,
      "name": "Damir Serik",
      "email": "damir@example.com",
      "photo": null,
      "telephony_enabled": false,
      "browser_registered": false,
      "extension": null,
      "external_enabled": false,
      "external_registered": false,
      "outbound": {
        "schemes": [],
        "primary_scheme_id": null,
        "primary_source": null,
        "falls_back_to": "legacy"
      }
    }
  ]
}

Enable telephony for a member

Request

bash
curl -X POST https://api.aisar.app/v1/companies/current/members/13/telephony/enable \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": {
    "message": "Telephony enabled.",
    "telephony": {
      "user_id": 13,
      "sip_username": "agent-13-web",
      "status": "active"
    }
  }
}

Member's SIP credentials (admin view)

Request

bash
curl https://api.aisar.app/v1/companies/current/members/13/telephony/credentials \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": {
    "sip_username": "agent-13",
    "sip_password": "Qw7Lm2VtRk4x",
    "host": "sip.aisar.app",
    "tcp_port": 5060,
    "tls_port": 5061,
    "default_transport": "TCP",
    "codecs": ["opus", "PCMU", "PCMA"],
    "media_encryption": "SRTP/SDES (over TLS)",
    "registered": false,
    "provisioning_uri": "sip:agent-13:Qw7Lm2VtRk4x@sip.aisar.app:5060;transport=tcp"
  }
}