Fellix Docs

REST API

Read and write your organization's clients, conversations, messages, bookings and reporting from your own systems.

v1 Download the OpenAPI document

Before you start

Base URL
https://api.fellix.ai — every path below hangs off it.
Getting a key
In the dashboard, under Settings → API. The owner of the organization creates it and chooses what it may reach. The key is shown once, at creation — we store only a hash of it and cannot show it again.
Authentication
Authorization: Bearer flx_live_…, or exchange an OAuth2 client id and secret at POST /v1/oauth/token for a one-hour token.

Revoking a key takes effect immediately, including for tokens already issued from it. If one leaks, revoke it and create another — there is nothing to wait for.

Guide

The Fellix REST API for your own systems. Everything here is scoped to the organization that issued the credential you send.

Base URL: https://api.fellix.ai — every path below hangs off it, so /v1/ping is https://api.fellix.ai/v1/ping.

Authentication

Two ways in, both on the Authorization header:

  • Static key — Authorization: Bearer flx_live_…. Created in the dashboard under Settings → API. Shown once, at creation; we store only a hash of it and cannot show it again. Lost means rotate.
  • OAuth2 client credentials — POST /v1/oauth/token with grant_type=client_credentials, client_id and client_secret, in exchange for a one-hour access token. No refresh token is issued (RFC 6749 §4.4.3); ask again with the same secret.

Revoking a credential takes effect immediately, including for access tokens already issued from it — an access token carries an identifier, not a copy of your permissions.

Scopes

Every credential carries a set of <resource>:<action> scopes. A request for something the credential was not granted answers 403, and the body names the scope it wanted. customers:write implies customers:read. GET /v1/account tells you what you hold.

Rate limits

Per organization, per endpoint, per minute: every credential your organization holds draws on the same allowance, so minting a second key does not double it. Every response to a credential we accepted carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (the Unix timestamp at which the current window ends); a 429 also carries Retry-After, in seconds. A 401 carries none of them — the bucket is named from the credential's organization, so a request we could not attribute has no bucket to report.

The window slides: the minute before the current one still counts, weighted by how much of it is still in view, so an allowance cannot be spent twice either side of a boundary. Heavier endpoints carry a proportionally lower ceiling; the one that applies is always the X-RateLimit-Limit that endpoint returns.

Two things are never charged for. A 5xx, because the failure is ours, and a 409 from an Idempotency-Key conflict, because nothing was performed — retry either without spending allowance.

A single credential can be held *below* the organization's ceiling, which is what to ask for when one integration is noisier than the rest. GET /v1/account reports the ceiling in force for the credential you are using.

Pagination

List endpoints are cursor-paginated. There is no page number.

GET /v1/customers?limit=50
→ { "data": [...], "meta": { ..., "pagination": { "next_cursor": "Ij...", "has_more": true } } }

GET /v1/customers?limit=50&after=Ij...
→ { "data": [...], "meta": { ..., "pagination": { "next_cursor": null, "has_more": false } } }

The cursor is opaque and signed — do not parse or construct one, just send back what the previous response gave you. Ordering is newest first (created_at DESC). A cursor we did not issue answers 400.

Relations

Nothing is embedded unless you ask. ?include=customer,tags embeds those relations; each endpoint documents which names it accepts, and an unknown one answers 400 rather than being quietly dropped. The same names configure what your webhook payloads carry, so a webhook and a GET of the same resource can be made to agree exactly.

Filtering

?filters[<field>_<predicate>]=<value>, combined with AND:

GET /v1/customers?filters[created_at_gteq]=2026-08-01T00:00:00Z&filters[name_cont]=acme

Predicates depend on the field's type — text takes eq|cont|start (case-insensitive), numbers and ids eq|in|gt|gteq|lt|lteq, timestamps gt|gteq|lt|lteq (ISO8601 only), enums eq|in. in takes a comma-separated list. Each endpoint documents its own fields. An unknown field, an unknown predicate or a value we cannot read answers 400 and names every offending key — nothing is ever silently ignored.

Keeping a copy in sync

There is no "changes since" endpoint and you do not need one. Every list endpoint takes filters[updated_at_gteq], and the order is newest first:

GET /v1/customers?filters[updated_at_gteq]=2026-09-25T00:00:00Z&limit=100

Store the timestamp you started the run at, not the newest updated_at you saw, and overlap the next window by a minute — a row written while you were paging through belongs to the next run, and an overlap costs you a duplicate you can ignore instead of a row you never see.

Rows are never hard-deleted from under you, but a client can be merged into another: follow merged_into_id and stop writing to the old one.

Writing

Create with POST, change with PUT. Bodies are namespaced by resource ({"customer": {...}}), and a field this API does not return is a field it does not accept — an unrecognised one answers 400 and names it rather than being ignored.

Retry safely with Idempotency-Key. A timeout leaves you unable to tell "never arrived" from "arrived, answer lost"; send a key you generate and a retry returns the first answer with Idempotency-Replayed: true instead of writing again. Reusing a key with a different body is a 409, because that is a bug worth being told about rather than papered over.

Two rules worth knowing before you send a message:

  • A thread a colleague has claimed refuses an API send (422). Someone is typing in it, and two voices from one window is worse than a refusal you can see.
  • On WhatsApp, 24 hours after the customer's last message only a template is delivered. That is what POST /v1/conversations/{id}/messages/template is for; a plain send after the window closes is accepted by nobody.

Messages sent this way carry no sender — the same shape the assistant's own replies use — and count as system messages, not as one of your agents' replies.

Reporting

Three aggregates over conversations: /v1/analytics/pipeline (where things stand), /v1/analytics/trends (the same won/lost as a series) and /v1/analytics/tags (volume and outcome per tag). All take start_date / end_date and default to the last 30 days, and every answer carries the window it used in meta.period — a figure without its window is not a figure.

They are narrow on purpose. Our own dashboard computes more than this, in shapes that change whenever a panel changes; what is published here is the stable core of each. They are also the most expensive thing on this API, so they carry a lower rate limit than a list.

leads and voice_calls are ordinary list endpoints rather than aggregates, each behind its own scope: a lead carries the campaign and ad ids you join your spend data against, and a call carries its transcript — which is what a real person said, and a different thing to be trusted with than a count.

Webhooks

The push half of this API. An organization subscribes its own HTTPS endpoints in the dashboard and receives HMAC-signed JSON — see WebhookEnvelope below for the wrapper.

data.attributes is the same shape as a GET of that resource. Not approximately: the two are serialized from one definition, so conversation.created carries exactly what GET /v1/conversations/{id} returns for the same relations. That is why you can act on a payload directly and only call back when you want something the event did not carry.

Embedded relations are per endpoint, and nothing is embedded by default. Each endpoint chooses which relations its payloads carry, per resource, from the same list ?include= accepts — so an endpoint configured with conversation: [customer, tags] receives exactly what ?include=customer,tags returns. Configure it in the dashboard; an endpoint that chooses nothing gets ids and attributes only, which is the cheapest thing to receive and usually enough to decide whether to call back.

Some events describe resources this surface has no GET for yet — voice_call.*, whatsapp_call.*, lead.created, team.*. Their shapes are PublicVoiceCall, PublicWhatsappCall, PublicWhatsappCallForward, PublicLead, PublicTeam and PublicTeamMember, documented here for that reason.

Dedupe on the envelope id (stable across endpoints and across redeliveries) and order on data.attributes.updated_at — delivery order is not guaranteed.

Errors

Everything except the OAuth token endpoint (which follows RFC 6749) answers in one shape:

{ "errors": [ { "title": "...", "detail": "...", "code": "not_found", "status": 404 } ] }

code is the stable, machine-readable part: auth-blank, auth-invalid, forbidden, not_found, invalid_cursor, invalid_parameter, too_many_requests.

Text follows Accept-Language where we have a translation; en-US otherwise.

Meta

GET /v1/ping #

Check a credential

Answers 200 for any live credential. Needs no scope — it is the call to make first, to confirm the key works and see the rate-limit headers.

Responses

200the credential is live

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicPing
metarequired Meta
401missing, unknown, revoked or expired credentialGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl https://api.fellix.ai/v1/ping \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": {
    "status": "ok",
    "organization_id": 41,
    "time": "2026-09-26T09:24:11Z"
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded"
  }
}

GET /v1/account #

Describe the calling credential

Who this credential belongs to and what it may do. Read this when something else answers 403.

Query parameters

FieldTypeDescription
include string

Relations to embed. Accepted: organization. Nothing is embedded unless named here.

Responses

200successful

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicAccount
metarequired Meta
400an include that this endpoint does not offerGeneralError
401missing, unknown, revoked or expired credentialGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl https://api.fellix.ai/v1/account \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": {
    "id": 1,
    "name": "Ada",
    "credential_type": "static",
    "scopes": [
      "customers:read"
    ],
    "prefix": "flx_live_9f2c",
    "client_id": "flx_cid_4b81",
    "expires_at": "2026-09-26T09:24:11Z",
    "last_used_at": "2026-09-26T09:24:11Z",
    "created_at": "2026-09-26T09:24:11Z",
    "rate_limits": {
      "requests_per_minute": 1,
      "window_seconds": 12,
      "credential_capped": true
    },
    "organization": {
      "id": 1,
      "name": "Ada",
      "time_zone": "Europe/Istanbul",
      "default_locale": "en-US",
      "created_at": "2026-09-26T09:24:11Z"
    }
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded"
  }
}

Authentication

POST /v1/oauth/token #

Exchange client credentials for an access tokenno credential

RFC 6749 client_credentials. Returns a one-hour bearer token and deliberately no refresh token (§4.4.3) — ask again with the same secret. Revoking the credential invalidates tokens already issued from it, immediately. This endpoint answers in the RFC 6749 shape, not the API error envelope.

Request body

FieldTypeDescription
grant_typerequired string client_credentials
client_idrequired string
client_secretrequired string

Responses

200a tokenOAuthToken
FieldTypeDescription
access_tokenrequired string
token_typerequired string Bearer
expires_inrequired integer

Seconds.

scope string

Space-separated, as RFC 6749 requires.

400an unsupported grant_typeOAuthError
401the client_id or client_secret is wrongOAuthError
cURL
curl -X POST https://api.fellix.ai/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
  "grant_type": "client_credentials",
  "client_id": "flx_cid_4b81",
  "client_secret": "flx_cs_9d2f7a13c5e84b60"
}'
Request body
{
  "grant_type": "client_credentials",
  "client_id": "flx_cid_4b81",
  "client_secret": "flx_cs_9d2f7a13c5e84b60"
}
200 response
{
  "access_token": "flx_at_9f2c8d41b7e34a6f",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "customers:read conversations:read"
}

Clients

GET /v1/customers #

list customerscustomers:read

The people your organization talks to. Poll filters[updated_at_gteq] to sync only what moved. Requires the customers:read scope.

Query parameters

FieldTypeDescription
limit integer 1–100

Page size. Default 50, maximum 100 (larger values are clamped, not rejected).

after string

The meta.pagination.next_cursor from the previous response, sent back unchanged.

include string

Relations to embed, comma-separated. Accepted: accounts. Nothing is embedded unless named here, and an unknown name is a 400.

filters object

Filters combine with AND. An unknown field, an unknown predicate or a value we cannot read answers 400 and names every offending key.

17 filters on this endpoint
filters[name_eq]stringname equals
filters[name_cont]stringname contains (case-insensitive)
filters[name_start]stringname starts with (case-insensitive)
filters[surname_eq]stringsurname equals
filters[surname_cont]stringsurname contains (case-insensitive)
filters[surname_start]stringsurname starts with (case-insensitive)
filters[email_eq]stringemail equals
filters[email_cont]stringemail contains (case-insensitive)
filters[email_start]stringemail starts with (case-insensitive)
filters[phone_eq]stringphone equals
filters[phone_cont]stringphone contains (case-insensitive)
filters[phone_start]stringphone starts with (case-insensitive)
filters[is_archived_eq]booleanis_archived equals — default: false
filters[created_at_gteq]string · date-timecreated_at greater than or equal to — ISO8601
filters[created_at_lteq]string · date-timecreated_at less than or equal to — ISO8601
filters[updated_at_gteq]string · date-timeupdated_at greater than or equal to — ISO8601
filters[updated_at_lteq]string · date-timeupdated_at less than or equal to — ISO8601

Responses

200a page of results

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicCustomer[]
metarequired MetaForCursorPagination
400a filter we do not understandGeneralError
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl https://api.fellix.ai/v1/customers?limit=50 \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": [
    {
      "id": 1,
      "name": "Ada",
      "surname": "Lovelace",
      "email": "ada@example.com",
      "phone": "+15550100",
      "description": "Referred by a colleague",
      "is_archived": false,
      "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
      "default_locale": "en-US",
      "time_zone": "Europe/Istanbul",
      "merged_into_id": 1,
      "created_at": "2026-09-26T09:24:11Z",
      "updated_at": "2026-09-26T09:24:11Z"
    }
  ],
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded",
    "pagination": {
      "next_cursor": "IjIwMjYtMDktMjZUMDk6MjQ6MTFaIg",
      "has_more": true
    }
  }
}

POST /v1/customers #

create a clientcustomers:write

Requires the customers:write scope. Email and phone are unique within the organization, so a duplicate answers 422 rather than creating a second record.

Headers

FieldTypeDescription
Idempotency-Key string max 255

Optional, and the only safe way to retry a write. Send a value you generate (a UUID). The first answer is stored for 24 hours and returned verbatim on a retry, with Idempotency-Replayed: true. The same key with a different body is refused (409). A 5xx releases the key, so retrying after one really does retry.

Request body

FieldTypeDescription
customerrequired object
namerequired string
surname string nullable
email string nullable

Unique within the organization

phone string nullable

Unique within the organization

description string nullable
is_archived boolean
whatsapp_opt_out_at string · date-time nullable

Set it to record a refusal you took yourself; clear it only on an explicit request from the person.

default_locale string nullable
time_zone string nullable

IANA name

Responses

201created

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicCustomer
metarequired Meta
400a field this API does not acceptGeneralError
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
409the Idempotency-Key was already used for a different request bodyGeneralError
422validation failedGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl -X POST https://api.fellix.ai/v1/customers \
  -H "Authorization: Bearer $FELLIX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "customer": {
    "name": "Ada",
    "surname": "Lovelace",
    "email": "ada@example.com",
    "phone": "+15550100",
    "description": "Referred by a colleague",
    "is_archived": false,
    "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
    "default_locale": "en-US",
    "time_zone": "Europe/Istanbul"
  }
}'
Request body
{
  "customer": {
    "name": "Ada",
    "surname": "Lovelace",
    "email": "ada@example.com",
    "phone": "+15550100",
    "description": "Referred by a colleague",
    "is_archived": false,
    "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
    "default_locale": "en-US",
    "time_zone": "Europe/Istanbul"
  }
}
201 response
{
  "data": {
    "id": 1,
    "name": "Ada",
    "surname": "Lovelace",
    "email": "ada@example.com",
    "phone": "+15550100",
    "description": "Referred by a colleague",
    "is_archived": false,
    "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
    "default_locale": "en-US",
    "time_zone": "Europe/Istanbul",
    "merged_into_id": 1,
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded"
  }
}

GET /v1/customers/{id} #

fetch one clientcustomers:read

Requires the customers:read scope. Another organization's id is a 404.

Query parameters

FieldTypeDescription
include string

Relations to embed, comma-separated. Accepted: accounts. Nothing is embedded unless named here, and an unknown name is a 400.

Responses

200successful

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicCustomer
metarequired Meta
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
404no such record for this organizationGeneralError
cURL
curl https://api.fellix.ai/v1/customers/8814 \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": {
    "id": 1,
    "name": "Ada",
    "surname": "Lovelace",
    "email": "ada@example.com",
    "phone": "+15550100",
    "description": "Referred by a colleague",
    "is_archived": false,
    "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
    "default_locale": "en-US",
    "time_zone": "Europe/Istanbul",
    "merged_into_id": 1,
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded"
  }
}

PUT /v1/customers/{id} #

update a clientcustomers:write

Requires the customers:write scope. Recorded in the activity feed as a change made by an integration, not by a member.

Headers

FieldTypeDescription
Idempotency-Key string max 255

Optional, and the only safe way to retry a write. Send a value you generate (a UUID). The first answer is stored for 24 hours and returned verbatim on a retry, with Idempotency-Replayed: true. The same key with a different body is refused (409). A 5xx releases the key, so retrying after one really does retry.

Request body

FieldTypeDescription
customerrequired object
namerequired string
surname string nullable
email string nullable

Unique within the organization

phone string nullable

Unique within the organization

description string nullable
is_archived boolean
whatsapp_opt_out_at string · date-time nullable

Set it to record a refusal you took yourself; clear it only on an explicit request from the person.

default_locale string nullable
time_zone string nullable

IANA name

Responses

200updated

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicCustomer
metarequired Meta
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
404no such client for this organizationGeneralError
409the Idempotency-Key was already used for a different request bodyGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl -X PUT https://api.fellix.ai/v1/customers/8814 \
  -H "Authorization: Bearer $FELLIX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "customer": {
    "name": "Ada",
    "surname": "Lovelace",
    "email": "ada@example.com",
    "phone": "+15550100",
    "description": "Referred by a colleague",
    "is_archived": false,
    "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
    "default_locale": "en-US",
    "time_zone": "Europe/Istanbul"
  }
}'
Request body
{
  "customer": {
    "name": "Ada",
    "surname": "Lovelace",
    "email": "ada@example.com",
    "phone": "+15550100",
    "description": "Referred by a colleague",
    "is_archived": false,
    "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
    "default_locale": "en-US",
    "time_zone": "Europe/Istanbul"
  }
}
200 response
{
  "data": {
    "id": 1,
    "name": "Ada",
    "surname": "Lovelace",
    "email": "ada@example.com",
    "phone": "+15550100",
    "description": "Referred by a colleague",
    "is_archived": false,
    "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
    "default_locale": "en-US",
    "time_zone": "Europe/Istanbul",
    "merged_into_id": 1,
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded"
  }
}

Conversations

GET /v1/conversations #

list conversationsconversations:read

One thread with one client on one channel. include=channel still names the channel after it is removed. Requires the conversations:read scope.

Query parameters

FieldTypeDescription
limit integer 1–100

Page size. Default 50, maximum 100 (larger values are clamped, not rejected).

after string

The meta.pagination.next_cursor from the previous response, sent back unchanged.

include string

Relations to embed, comma-separated. Accepted: customer, channel, tags, last_message. Nothing is embedded unless named here, and an unknown name is a 400.

filters object

Filters combine with AND. An unknown field, an unknown predicate or a value we cannot read answers 400 and names every offending key.

18 filters on this endpoint
filters[customer_id_eq]integercustomer_id equals
filters[customer_id_in]stringcustomer_id one of (comma-separated)
filters[channel_id_eq]integerchannel_id equals
filters[channel_id_in]stringchannel_id one of (comma-separated)
filters[status_eq]stringstatus equals — values: active|completed|escalated|abandoned|blocked
filters[status_in]stringstatus one of (comma-separated) — values: active|completed|escalated|abandoned|blocked
filters[lead_status_eq]stringlead_status equals — values: open|won|lost
filters[lead_status_in]stringlead_status one of (comma-separated) — values: open|won|lost
filters[qualification_eq]stringqualification equals — values: hot|warm|cold
filters[qualification_in]stringqualification one of (comma-separated) — values: hot|warm|cold
filters[language_eq]stringlanguage equals
filters[language_cont]stringlanguage contains (case-insensitive)
filters[language_start]stringlanguage starts with (case-insensitive)
filters[is_archived_eq]booleanis_archived equals — default: false
filters[created_at_gteq]string · date-timecreated_at greater than or equal to — ISO8601
filters[created_at_lteq]string · date-timecreated_at less than or equal to — ISO8601
filters[updated_at_gteq]string · date-timeupdated_at greater than or equal to — ISO8601
filters[updated_at_lteq]string · date-timeupdated_at less than or equal to — ISO8601

Responses

200a page of results

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicConversation[]
metarequired MetaForCursorPagination
400a filter we do not understandGeneralError
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl https://api.fellix.ai/v1/conversations?limit=50 \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": [
    {
      "id": 1,
      "customer_id": 1,
      "channel_id": 1,
      "status": "active",
      "is_archived": false,
      "language": "en",
      "lead_status": "open",
      "qualification": "hot",
      "lost_reason": "string",
      "rating": 1,
      "started_at": "2026-09-26T09:24:11Z",
      "ended_at": "2026-09-26T09:24:11Z",
      "created_at": "2026-09-26T09:24:11Z",
      "updated_at": "2026-09-26T09:24:11Z"
    }
  ],
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded",
    "pagination": {
      "next_cursor": "IjIwMjYtMDktMjZUMDk6MjQ6MTFaIg",
      "has_more": true
    }
  }
}

GET /v1/conversations/{id} #

fetch one conversationconversations:read

Requires the conversations:read scope. Another organization's id is a 404.

Query parameters

FieldTypeDescription
include string

Relations to embed, comma-separated. Accepted: customer, channel, tags, last_message. Nothing is embedded unless named here, and an unknown name is a 400.

Responses

200successful

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicConversation
metarequired Meta
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
404no such record for this organizationGeneralError
cURL
curl https://api.fellix.ai/v1/conversations/8814 \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": {
    "id": 1,
    "customer_id": 1,
    "channel_id": 1,
    "status": "active",
    "is_archived": false,
    "language": "en",
    "lead_status": "open",
    "qualification": "hot",
    "lost_reason": "string",
    "rating": 1,
    "started_at": "2026-09-26T09:24:11Z",
    "ended_at": "2026-09-26T09:24:11Z",
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded"
  }
}

Messages

GET /v1/messages #

list messagesmessages:read

Narrow to one thread with filters[conversation_id_eq]. Attachments are not part of v1. Requires the messages:read scope.

Query parameters

FieldTypeDescription
limit integer 1–100

Page size. Default 50, maximum 100 (larger values are clamped, not rejected).

after string

The meta.pagination.next_cursor from the previous response, sent back unchanged.

include string

Relations to embed, comma-separated. Accepted: conversation. Nothing is embedded unless named here, and an unknown name is a 400.

filters object

Filters combine with AND. An unknown field, an unknown predicate or a value we cannot read answers 400 and names every offending key.

12 filters on this endpoint
filters[conversation_id_eq]integerconversation_id equals
filters[conversation_id_in]stringconversation_id one of (comma-separated)
filters[message_type_eq]stringmessage_type equals — values: text|image|video|audio|document|location|contact|poll|event|reaction|sticker|reply|template|interactive|system
filters[message_type_in]stringmessage_type one of (comma-separated) — values: text|image|video|audio|document|location|contact|poll|event|reaction|sticker|reply|template|interactive|system
filters[from_customer_eq]booleanfrom_customer equals
filters[external_id_eq]stringexternal_id equals
filters[sent_at_gteq]string · date-timesent_at greater than or equal to — ISO8601
filters[sent_at_lteq]string · date-timesent_at less than or equal to — ISO8601
filters[created_at_gteq]string · date-timecreated_at greater than or equal to — ISO8601
filters[created_at_lteq]string · date-timecreated_at less than or equal to — ISO8601
filters[updated_at_gteq]string · date-timeupdated_at greater than or equal to — ISO8601
filters[updated_at_lteq]string · date-timeupdated_at less than or equal to — ISO8601

Responses

200a page of results

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicMessage[]
metarequired MetaForCursorPagination
400a filter we do not understandGeneralError
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl https://api.fellix.ai/v1/messages?limit=50 \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": [
    {
      "id": 1,
      "conversation_id": 1,
      "message_type": "text",
      "from_customer": true,
      "content": "On our way.",
      "language": "en",
      "external_id": "msg_9f2c8d41",
      "reply_to_id": 1,
      "delivery_status": "sent",
      "sent_at": "2026-09-26T09:24:11Z",
      "created_at": "2026-09-26T09:24:11Z",
      "updated_at": "2026-09-26T09:24:11Z"
    }
  ],
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded",
    "pagination": {
      "next_cursor": "IjIwMjYtMDktMjZUMDk6MjQ6MTFaIg",
      "has_more": true
    }
  }
}

GET /v1/messages/{id} #

fetch one messagemessages:read

Requires the messages:read scope. Another organization's id is a 404.

Query parameters

FieldTypeDescription
include string

Relations to embed, comma-separated. Accepted: conversation. Nothing is embedded unless named here, and an unknown name is a 400.

Responses

200successful

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicMessage
metarequired Meta
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
404no such record for this organizationGeneralError
cURL
curl https://api.fellix.ai/v1/messages/8814 \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": {
    "id": 1,
    "conversation_id": 1,
    "message_type": "text",
    "from_customer": true,
    "content": "On our way.",
    "language": "en",
    "external_id": "msg_9f2c8d41",
    "reply_to_id": 1,
    "delivery_status": "sent",
    "sent_at": "2026-09-26T09:24:11Z",
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded"
  }
}

POST /v1/conversations/{conversation_id}/messages #

send a messagemessages:write

Requires the messages:write scope. Sent with no sender, the same shape the assistant's own replies use. Refused with 422 when a colleague has claimed the thread, when it is archived, or when the channel cannot send. On WhatsApp, after 24 hours of customer silence use the template endpoint instead.

Headers

FieldTypeDescription
Idempotency-Key string max 255

Optional, and the only safe way to retry a write. Send a value you generate (a UUID). The first answer is stored for 24 hours and returned verbatim on a retry, with Idempotency-Replayed: true. The same key with a different body is refused (409). A 5xx releases the key, so retrying after one really does retry.

Request body

FieldTypeDescription
messagerequired object
content string

Length limits are the platform's: 4096 on WhatsApp, 2000 on Instagram.

file string · binary

Send as multipart/form-data. Website chat channels only — the other platforms' send paths carry text, so a file accepted there would be stored and never delivered.

Responses

201sent

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicMessage
metarequired Meta
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
404no such conversation for this organizationGeneralError
409the Idempotency-Key was already used for a different request bodyGeneralError
422the thread or the channel refused itGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl -X POST https://api.fellix.ai/v1/conversations/4412/messages \
  -H "Authorization: Bearer $FELLIX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "message": {
    "content": "On our way.",
    "file": "<binary>"
  }
}'
Request body
{
  "message": {
    "content": "On our way.",
    "file": "<binary>"
  }
}
201 response
{
  "data": {
    "id": 1,
    "conversation_id": 1,
    "message_type": "text",
    "from_customer": true,
    "content": "On our way.",
    "language": "en",
    "external_id": "msg_9f2c8d41",
    "reply_to_id": 1,
    "delivery_status": "sent",
    "sent_at": "2026-09-26T09:24:11Z",
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded"
  }
}

POST /v1/conversations/{conversation_id}/messages/template #

send a WhatsApp templatemessages:write

Requires the messages:write scope. WhatsApp only, and the only thing Meta delivers once 24 hours have passed since the customer last wrote — which is why an appointment reminder has to go this way. 422 names the reason when the channel, its credential or the thread is not in a state to send.

Headers

FieldTypeDescription
Idempotency-Key string max 255

Optional, and the only safe way to retry a write. Send a value you generate (a UUID). The first answer is stored for 24 hours and returned verbatim on a retry, with Idempotency-Replayed: true. The same key with a different body is refused (409). A 5xx releases the key, so retrying after one really does retry.

Request body

FieldTypeDescription
templaterequired object
namerequired string

The approved template name as WhatsApp knows it

languagerequired string

e.g. en_US

components object[]

Body/header parameters, in Meta's own component shape

Responses

201sent

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicMessage
metarequired Meta
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
409the Idempotency-Key was already used for a different request bodyGeneralError
422this channel cannot send a templateGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl -X POST https://api.fellix.ai/v1/conversations/4412/messages/template \
  -H "Authorization: Bearer $FELLIX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "template": {
    "name": "Ada",
    "language": "en",
    "components": [
      {}
    ]
  }
}'
Request body
{
  "template": {
    "name": "Ada",
    "language": "en",
    "components": [
      {}
    ]
  }
}
201 response
{
  "data": {
    "id": 1,
    "conversation_id": 1,
    "message_type": "text",
    "from_customer": true,
    "content": "On our way.",
    "language": "en",
    "external_id": "msg_9f2c8d41",
    "reply_to_id": 1,
    "delivery_status": "sent",
    "sent_at": "2026-09-26T09:24:11Z",
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded"
  }
}

Tags

GET /v1/tags #

list tagstags:read

The organization's own taxonomy. system: true marks the ones Fellix maintains. Requires the tags:read scope.

Query parameters

FieldTypeDescription
limit integer 1–100

Page size. Default 50, maximum 100 (larger values are clamped, not rejected).

after string

The meta.pagination.next_cursor from the previous response, sent back unchanged.

include string

This resource embeds no relations.

filters object

Filters combine with AND. An unknown field, an unknown predicate or a value we cannot read answers 400 and names every offending key.

11 filters on this endpoint
filters[name_eq]stringname equals
filters[name_cont]stringname contains (case-insensitive)
filters[name_start]stringname starts with (case-insensitive)
filters[category_key_eq]stringcategory_key equals
filters[category_key_cont]stringcategory_key contains (case-insensitive)
filters[category_key_start]stringcategory_key starts with (case-insensitive)
filters[system_eq]booleansystem equals
filters[created_at_gteq]string · date-timecreated_at greater than or equal to — ISO8601
filters[created_at_lteq]string · date-timecreated_at less than or equal to — ISO8601
filters[updated_at_gteq]string · date-timeupdated_at greater than or equal to — ISO8601
filters[updated_at_lteq]string · date-timeupdated_at less than or equal to — ISO8601

Responses

200a page of results

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicTag[]
metarequired MetaForCursorPagination
400a filter we do not understandGeneralError
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl https://api.fellix.ai/v1/tags?limit=50 \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": [
    {
      "id": 1,
      "name": "Ada",
      "color": "string",
      "category_key": "string",
      "description": "Referred by a colleague",
      "system": true,
      "created_at": "2026-09-26T09:24:11Z",
      "updated_at": "2026-09-26T09:24:11Z"
    }
  ],
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded",
    "pagination": {
      "next_cursor": "IjIwMjYtMDktMjZUMDk6MjQ6MTFaIg",
      "has_more": true
    }
  }
}

GET /v1/tags/{id} #

fetch one tagtags:read

Requires the tags:read scope. Another organization's id is a 404.

Query parameters

FieldTypeDescription
include string

This resource embeds no relations.

Responses

200successful

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicTag
metarequired Meta
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
404no such record for this organizationGeneralError
cURL
curl https://api.fellix.ai/v1/tags/8814 \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": {
    "id": 1,
    "name": "Ada",
    "color": "string",
    "category_key": "string",
    "description": "Referred by a colleague",
    "system": true,
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded"
  }
}

Appointments

GET /v1/appointments #

list appointmentsappointments:read

Bookings. filters[starts_at_gteq] + filters[starts_at_lteq] is the calendar window query. Requires the appointments:read scope.

Query parameters

FieldTypeDescription
limit integer 1–100

Page size. Default 50, maximum 100 (larger values are clamped, not rejected).

after string

The meta.pagination.next_cursor from the previous response, sent back unchanged.

include string

Relations to embed, comma-separated. Accepted: customer. Nothing is embedded unless named here, and an unknown name is a 400.

filters object

Filters combine with AND. An unknown field, an unknown predicate or a value we cannot read answers 400 and names every offending key.

16 filters on this endpoint
filters[customer_id_eq]integercustomer_id equals
filters[customer_id_in]stringcustomer_id one of (comma-separated)
filters[conversation_id_eq]integerconversation_id equals
filters[conversation_id_in]stringconversation_id one of (comma-separated)
filters[status_eq]stringstatus equals — values: requested|confirmed|cancelled|completed|no_show
filters[status_in]stringstatus one of (comma-separated) — values: requested|confirmed|cancelled|completed|no_show
filters[source_eq]stringsource equals — values: operator|assistant|api
filters[source_in]stringsource one of (comma-separated) — values: operator|assistant|api
filters[starts_at_gteq]string · date-timestarts_at greater than or equal to — ISO8601
filters[starts_at_lteq]string · date-timestarts_at less than or equal to — ISO8601
filters[ends_at_gteq]string · date-timeends_at greater than or equal to — ISO8601
filters[ends_at_lteq]string · date-timeends_at less than or equal to — ISO8601
filters[created_at_gteq]string · date-timecreated_at greater than or equal to — ISO8601
filters[created_at_lteq]string · date-timecreated_at less than or equal to — ISO8601
filters[updated_at_gteq]string · date-timeupdated_at greater than or equal to — ISO8601
filters[updated_at_lteq]string · date-timeupdated_at less than or equal to — ISO8601

Responses

200a page of results

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicAppointment[]
metarequired MetaForCursorPagination
400a filter we do not understandGeneralError
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl https://api.fellix.ai/v1/appointments?limit=50 \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": [
    {
      "id": 1,
      "customer_id": 1,
      "conversation_id": 1,
      "channel_id": 1,
      "starts_at": "2026-09-26T09:24:11Z",
      "ends_at": "2026-09-26T09:24:11Z",
      "status": "requested",
      "source": "operator",
      "notes": "Prefers the afternoon",
      "created_at": "2026-09-26T09:24:11Z",
      "updated_at": "2026-09-26T09:24:11Z"
    }
  ],
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded",
    "pagination": {
      "next_cursor": "IjIwMjYtMDktMjZUMDk6MjQ6MTFaIg",
      "has_more": true
    }
  }
}

POST /v1/appointments #

create a bookingappointments:write

Requires the appointments:write scope. Arrives as source: api and status: confirmed. Capacity and opening-hours refusals come back as 422 with the reason.

Headers

FieldTypeDescription
Idempotency-Key string max 255

Optional, and the only safe way to retry a write. Send a value you generate (a UUID). The first answer is stored for 24 hours and returned verbatim on a retry, with Idempotency-Replayed: true. The same key with a different body is refused (409). A 5xx releases the key, so retrying after one really does retry.

Request body

FieldTypeDescription
appointmentrequired object
customer_idrequired integer
conversation_id integer nullable
channel_id integer nullable
starts_atrequired string · date-time
ends_at string · date-time nullable

Omit and it becomes one slot of the organization's configured length. Moving starts_at without an end keeps the same duration.

status string requested confirmed cancelled completed no_show

Cancel by setting cancelled. There is no DELETE — a cancelled booking is kept.

notes string nullable

Responses

201created

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicAppointment
metarequired Meta
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
409the Idempotency-Key was already used for a different request bodyGeneralError
422validation failedGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl -X POST https://api.fellix.ai/v1/appointments \
  -H "Authorization: Bearer $FELLIX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "appointment": {
    "customer_id": 1,
    "conversation_id": 1,
    "channel_id": 1,
    "starts_at": "2026-09-26T09:24:11Z",
    "ends_at": "2026-09-26T09:24:11Z",
    "status": "requested",
    "notes": "Prefers the afternoon"
  }
}'
Request body
{
  "appointment": {
    "customer_id": 1,
    "conversation_id": 1,
    "channel_id": 1,
    "starts_at": "2026-09-26T09:24:11Z",
    "ends_at": "2026-09-26T09:24:11Z",
    "status": "requested",
    "notes": "Prefers the afternoon"
  }
}
201 response
{
  "data": {
    "id": 1,
    "customer_id": 1,
    "conversation_id": 1,
    "channel_id": 1,
    "starts_at": "2026-09-26T09:24:11Z",
    "ends_at": "2026-09-26T09:24:11Z",
    "status": "requested",
    "source": "operator",
    "notes": "Prefers the afternoon",
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded"
  }
}

GET /v1/appointments/{id} #

fetch one appointmentappointments:read

Requires the appointments:read scope. Another organization's id is a 404.

Query parameters

FieldTypeDescription
include string

Relations to embed, comma-separated. Accepted: customer. Nothing is embedded unless named here, and an unknown name is a 400.

Responses

200successful

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicAppointment
metarequired Meta
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
404no such record for this organizationGeneralError
cURL
curl https://api.fellix.ai/v1/appointments/8814 \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": {
    "id": 1,
    "customer_id": 1,
    "conversation_id": 1,
    "channel_id": 1,
    "starts_at": "2026-09-26T09:24:11Z",
    "ends_at": "2026-09-26T09:24:11Z",
    "status": "requested",
    "source": "operator",
    "notes": "Prefers the afternoon",
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded"
  }
}

PUT /v1/appointments/{id} #

update or cancel a bookingappointments:write

Requires the appointments:write scope. Cancel with status: "cancelled" — there is no DELETE, because a cancelled booking is a fact the calendar keeps. Moving starts_at without an end keeps the same duration.

Headers

FieldTypeDescription
Idempotency-Key string max 255

Optional, and the only safe way to retry a write. Send a value you generate (a UUID). The first answer is stored for 24 hours and returned verbatim on a retry, with Idempotency-Replayed: true. The same key with a different body is refused (409). A 5xx releases the key, so retrying after one really does retry.

Request body

FieldTypeDescription
appointmentrequired object
customer_idrequired integer
conversation_id integer nullable
channel_id integer nullable
starts_atrequired string · date-time
ends_at string · date-time nullable

Omit and it becomes one slot of the organization's configured length. Moving starts_at without an end keeps the same duration.

status string requested confirmed cancelled completed no_show

Cancel by setting cancelled. There is no DELETE — a cancelled booking is kept.

notes string nullable

Responses

200updated

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicAppointment
metarequired Meta
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
404no such booking for this organizationGeneralError
409the Idempotency-Key was already used for a different request bodyGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl -X PUT https://api.fellix.ai/v1/appointments/8814 \
  -H "Authorization: Bearer $FELLIX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "appointment": {
    "customer_id": 1,
    "conversation_id": 1,
    "channel_id": 1,
    "starts_at": "2026-09-26T09:24:11Z",
    "ends_at": "2026-09-26T09:24:11Z",
    "status": "requested",
    "notes": "Prefers the afternoon"
  }
}'
Request body
{
  "appointment": {
    "customer_id": 1,
    "conversation_id": 1,
    "channel_id": 1,
    "starts_at": "2026-09-26T09:24:11Z",
    "ends_at": "2026-09-26T09:24:11Z",
    "status": "requested",
    "notes": "Prefers the afternoon"
  }
}
200 response
{
  "data": {
    "id": 1,
    "customer_id": 1,
    "conversation_id": 1,
    "channel_id": 1,
    "starts_at": "2026-09-26T09:24:11Z",
    "ends_at": "2026-09-26T09:24:11Z",
    "status": "requested",
    "source": "operator",
    "notes": "Prefers the afternoon",
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded"
  }
}

Leads

GET /v1/leads #

list leadsleads:read

Ad and form submissions. Read-only — a lead arrives from the ad platform, not from you. campaign_id and ad_id are what you join your own spend data against. Requires the leads:read scope.

Query parameters

FieldTypeDescription
limit integer 1–100

Page size. Default 50, maximum 100 (larger values are clamped, not rejected).

after string

The meta.pagination.next_cursor from the previous response, sent back unchanged.

include string

Relations to embed, comma-separated. Accepted: customer, conversation. Nothing is embedded unless named here, and an unknown name is a 400.

filters object

Filters combine with AND. An unknown field, an unknown predicate or a value we cannot read answers 400 and names every offending key.

20 filters on this endpoint
filters[source_eq]stringsource equals — values: meta_lead_ads|meta_ctwa|meta_ctm|meta_organic|native_form|manual
filters[source_in]stringsource one of (comma-separated) — values: meta_lead_ads|meta_ctwa|meta_ctm|meta_organic|native_form|manual
filters[status_eq]stringstatus equals — values: new|processed|failed
filters[status_in]stringstatus one of (comma-separated) — values: new|processed|failed
filters[platform_eq]stringplatform equals
filters[platform_cont]stringplatform contains (case-insensitive)
filters[platform_start]stringplatform starts with (case-insensitive)
filters[campaign_id_eq]stringcampaign_id equals
filters[ad_id_eq]stringad_id equals
filters[form_id_eq]stringform_id equals
filters[customer_id_eq]integercustomer_id equals
filters[customer_id_in]stringcustomer_id one of (comma-separated)
filters[conversation_id_eq]integerconversation_id equals
filters[conversation_id_in]stringconversation_id one of (comma-separated)
filters[submitted_at_gteq]string · date-timesubmitted_at greater than or equal to — ISO8601
filters[submitted_at_lteq]string · date-timesubmitted_at less than or equal to — ISO8601
filters[created_at_gteq]string · date-timecreated_at greater than or equal to — ISO8601
filters[created_at_lteq]string · date-timecreated_at less than or equal to — ISO8601
filters[updated_at_gteq]string · date-timeupdated_at greater than or equal to — ISO8601
filters[updated_at_lteq]string · date-timeupdated_at less than or equal to — ISO8601

Responses

200a page of results

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicLead[]
metarequired MetaForCursorPagination
400a filter we do not understandGeneralError
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl https://api.fellix.ai/v1/leads?limit=50 \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": [
    {
      "id": 1,
      "source": "meta_lead_ads",
      "platform": "string",
      "status": "new",
      "external_lead_id": "msg_9f2c8d41",
      "form_id": "msg_9f2c8d41",
      "form_name": "string",
      "ad_id": "msg_9f2c8d41",
      "ad_name": "string",
      "adset_id": "msg_9f2c8d41",
      "adset_name": "string",
      "campaign_id": "msg_9f2c8d41",
      "campaign_name": "string",
      "submitted_at": "2026-09-26T09:24:11Z",
      "customer_id": 1,
      "conversation_id": 1,
      "created_at": "2026-09-26T09:24:11Z",
      "updated_at": "2026-09-26T09:24:11Z",
      "customer": {
        "id": 1,
        "name": "Ada",
        "surname": "Lovelace",
        "email": "ada@example.com",
        "phone": "+15550100",
        "description": "Referred by a colleague",
        "is_archived": false,
        "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
        "default_locale": "en-US",
        "time_zone": "Europe/Istanbul",
        "merged_into_id": 1,
        "created_at": "2026-09-26T09:24:11Z",
        "updated_at": "2026-09-26T09:24:11Z"
      },
      "conversation": {
        "id": 1,
        "customer_id": 1,
        "channel_id": 1,
        "status": "active",
        "is_archived": false,
        "language": "en",
        "lead_status": "open",
        "qualification": "hot",
        "lost_reason": "string",
        "rating": 1,
        "started_at": "2026-09-26T09:24:11Z",
        "ended_at": "2026-09-26T09:24:11Z",
        "created_at": "2026-09-26T09:24:11Z",
        "updated_at": "2026-09-26T09:24:11Z"
      }
    }
  ],
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded",
    "pagination": {
      "next_cursor": "IjIwMjYtMDktMjZUMDk6MjQ6MTFaIg",
      "has_more": true
    }
  }
}

GET /v1/leads/{id} #

fetch one leadleads:read

Requires the leads:read scope. Another organization's id is a 404.

Query parameters

FieldTypeDescription
include string

Relations to embed, comma-separated. Accepted: customer, conversation. Nothing is embedded unless named here, and an unknown name is a 400.

Responses

200successful

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicLead
metarequired Meta
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
404no such record for this organizationGeneralError
cURL
curl https://api.fellix.ai/v1/leads/8814 \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": {
    "id": 1,
    "source": "meta_lead_ads",
    "platform": "string",
    "status": "new",
    "external_lead_id": "msg_9f2c8d41",
    "form_id": "msg_9f2c8d41",
    "form_name": "string",
    "ad_id": "msg_9f2c8d41",
    "ad_name": "string",
    "adset_id": "msg_9f2c8d41",
    "adset_name": "string",
    "campaign_id": "msg_9f2c8d41",
    "campaign_name": "string",
    "submitted_at": "2026-09-26T09:24:11Z",
    "customer_id": 1,
    "conversation_id": 1,
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z",
    "customer": {
      "id": 1,
      "name": "Ada",
      "surname": "Lovelace",
      "email": "ada@example.com",
      "phone": "+15550100",
      "description": "Referred by a colleague",
      "is_archived": false,
      "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
      "default_locale": "en-US",
      "time_zone": "Europe/Istanbul",
      "merged_into_id": 1,
      "created_at": "2026-09-26T09:24:11Z",
      "updated_at": "2026-09-26T09:24:11Z"
    },
    "conversation": {
      "id": 1,
      "customer_id": 1,
      "channel_id": 1,
      "status": "active",
      "is_archived": false,
      "language": "en",
      "lead_status": "open",
      "qualification": "hot",
      "lost_reason": "string",
      "rating": 1,
      "started_at": "2026-09-26T09:24:11Z",
      "ended_at": "2026-09-26T09:24:11Z",
      "created_at": "2026-09-26T09:24:11Z",
      "updated_at": "2026-09-26T09:24:11Z"
    }
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded"
  }
}

Voice calls

GET /v1/voice_calls #

list voice_callsvoice_calls:read

Call records with their transcript. Read-only, and its own scope rather than analytics:read — a transcript is what a real person said, not a count. The recording itself is not part of v1. Requires the voice_calls:read scope.

Query parameters

FieldTypeDescription
limit integer 1–100

Page size. Default 50, maximum 100 (larger values are clamped, not rejected).

after string

The meta.pagination.next_cursor from the previous response, sent back unchanged.

include string

Relations to embed, comma-separated. Accepted: customer, conversation. Nothing is embedded unless named here, and an unknown name is a 400.

filters object

Filters combine with AND. An unknown field, an unknown predicate or a value we cannot read answers 400 and names every offending key.

23 filters on this endpoint
filters[status_eq]stringstatus equals — values: pending|ringing|in_progress|completed|failed|no_answer|busy|canceled
filters[status_in]stringstatus one of (comma-separated) — values: pending|ringing|in_progress|completed|failed|no_answer|busy|canceled
filters[direction_eq]stringdirection equals — values: inbound|outbound
filters[direction_in]stringdirection one of (comma-separated) — values: inbound|outbound
filters[external_call_id_eq]stringexternal_call_id equals
filters[from_number_eq]stringfrom_number equals
filters[from_number_cont]stringfrom_number contains (case-insensitive)
filters[from_number_start]stringfrom_number starts with (case-insensitive)
filters[to_number_eq]stringto_number equals
filters[to_number_cont]stringto_number contains (case-insensitive)
filters[to_number_start]stringto_number starts with (case-insensitive)
filters[customer_id_eq]integercustomer_id equals
filters[customer_id_in]stringcustomer_id one of (comma-separated)
filters[conversation_id_eq]integerconversation_id equals
filters[conversation_id_in]stringconversation_id one of (comma-separated)
filters[duration_seconds_gteq]integerduration_seconds greater than or equal to
filters[duration_seconds_lteq]integerduration_seconds less than or equal to
filters[started_at_gteq]string · date-timestarted_at greater than or equal to — ISO8601
filters[started_at_lteq]string · date-timestarted_at less than or equal to — ISO8601
filters[created_at_gteq]string · date-timecreated_at greater than or equal to — ISO8601
filters[created_at_lteq]string · date-timecreated_at less than or equal to — ISO8601
filters[updated_at_gteq]string · date-timeupdated_at greater than or equal to — ISO8601
filters[updated_at_lteq]string · date-timeupdated_at less than or equal to — ISO8601

Responses

200a page of results

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicVoiceCall[]
metarequired MetaForCursorPagination
400a filter we do not understandGeneralError
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl https://api.fellix.ai/v1/voice_calls?limit=50 \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": [
    {
      "id": 1,
      "external_call_id": "msg_9f2c8d41",
      "status": "pending",
      "direction": "inbound",
      "from_number": "string",
      "to_number": "string",
      "started_at": "2026-09-26T09:24:11Z",
      "ended_at": "2026-09-26T09:24:11Z",
      "duration_seconds": 12,
      "transcript": "Hello, I would like to move my appointment.",
      "customer_id": 1,
      "conversation_id": 1,
      "created_at": "2026-09-26T09:24:11Z",
      "updated_at": "2026-09-26T09:24:11Z",
      "customer": {
        "id": 1,
        "name": "Ada",
        "surname": "Lovelace",
        "email": "ada@example.com",
        "phone": "+15550100",
        "description": "Referred by a colleague",
        "is_archived": false,
        "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
        "default_locale": "en-US",
        "time_zone": "Europe/Istanbul",
        "merged_into_id": 1,
        "created_at": "2026-09-26T09:24:11Z",
        "updated_at": "2026-09-26T09:24:11Z"
      },
      "conversation": {
        "id": 1,
        "customer_id": 1,
        "channel_id": 1,
        "status": "active",
        "is_archived": false,
        "language": "en",
        "lead_status": "open",
        "qualification": "hot",
        "lost_reason": "string",
        "rating": 1,
        "started_at": "2026-09-26T09:24:11Z",
        "ended_at": "2026-09-26T09:24:11Z",
        "created_at": "2026-09-26T09:24:11Z",
        "updated_at": "2026-09-26T09:24:11Z"
      }
    }
  ],
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded",
    "pagination": {
      "next_cursor": "IjIwMjYtMDktMjZUMDk6MjQ6MTFaIg",
      "has_more": true
    }
  }
}

GET /v1/voice_calls/{id} #

fetch one voice_callvoice_calls:read

Requires the voice_calls:read scope. Another organization's id is a 404.

Query parameters

FieldTypeDescription
include string

Relations to embed, comma-separated. Accepted: customer, conversation. Nothing is embedded unless named here, and an unknown name is a 400.

Responses

200successful

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicVoiceCall
metarequired Meta
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
404no such record for this organizationGeneralError
cURL
curl https://api.fellix.ai/v1/voice_calls/8814 \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": {
    "id": 1,
    "external_call_id": "msg_9f2c8d41",
    "status": "pending",
    "direction": "inbound",
    "from_number": "string",
    "to_number": "string",
    "started_at": "2026-09-26T09:24:11Z",
    "ended_at": "2026-09-26T09:24:11Z",
    "duration_seconds": 12,
    "transcript": "Hello, I would like to move my appointment.",
    "customer_id": 1,
    "conversation_id": 1,
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z",
    "customer": {
      "id": 1,
      "name": "Ada",
      "surname": "Lovelace",
      "email": "ada@example.com",
      "phone": "+15550100",
      "description": "Referred by a colleague",
      "is_archived": false,
      "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
      "default_locale": "en-US",
      "time_zone": "Europe/Istanbul",
      "merged_into_id": 1,
      "created_at": "2026-09-26T09:24:11Z",
      "updated_at": "2026-09-26T09:24:11Z"
    },
    "conversation": {
      "id": 1,
      "customer_id": 1,
      "channel_id": 1,
      "status": "active",
      "is_archived": false,
      "language": "en",
      "lead_status": "open",
      "qualification": "hot",
      "lost_reason": "string",
      "rating": 1,
      "started_at": "2026-09-26T09:24:11Z",
      "ended_at": "2026-09-26T09:24:11Z",
      "created_at": "2026-09-26T09:24:11Z",
      "updated_at": "2026-09-26T09:24:11Z"
    }
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded"
  }
}

Reporting

GET /v1/analytics/pipeline #

pipeline reportanalytics:read

Where the pipeline stands, and how long it took to get there. Narrower than our own dashboard computes, on purpose: its shapes change whenever a panel does, and this one is frozen. Requires the analytics:read scope.

Query parameters

FieldTypeDescription
start_date string · date

ISO8601 date (YYYY-MM-DD). Defaults to 30 days ago. Anything else is a 400 — a date we guessed at is a window you never asked for.

end_date string · date

ISO8601 date (YYYY-MM-DD). Defaults to today.

granularity string day week month

Bucket size for the series. An unrecognised value falls back to day, and the response says which one it used.

Responses

200the report

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicPipelineAnalytics
metarequired MetaForPeriod
400a date that is not ISO8601GeneralError
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl https://api.fellix.ai/v1/analytics/pipeline \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": {
    "open": 12,
    "won": 12,
    "lost": 12,
    "win_rate": 0.42,
    "avg_days_to_won": 3.5,
    "avg_days_to_lost": 3.5,
    "by_lost_reason": {},
    "by_qualification": {}
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded",
    "period": {
      "start": "2026-09-26T09:24:11Z",
      "end": "2026-09-26T09:24:11Z",
      "granularity": "day"
    }
  }
}

GET /v1/analytics/trends #

trends reportanalytics:read

The same won/lost as a series, bucketed by granularity. The response echoes the granularity it used, because an unrecognised request falls back rather than erroring. Requires the analytics:read scope.

Query parameters

FieldTypeDescription
start_date string · date

ISO8601 date (YYYY-MM-DD). Defaults to 30 days ago. Anything else is a 400 — a date we guessed at is a window you never asked for.

end_date string · date

ISO8601 date (YYYY-MM-DD). Defaults to today.

granularity string day week month

Bucket size for the series. An unrecognised value falls back to day, and the response says which one it used.

Responses

200the report

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicTrendAnalytics
metarequired MetaForPeriod
400a date that is not ISO8601GeneralError
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl https://api.fellix.ai/v1/analytics/trends \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": {
    "granularity": "day",
    "points": [
      {
        "period": "2026-09-26T09:24:11Z",
        "won": 12,
        "lost": 12
      }
    ]
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded",
    "period": {
      "start": "2026-09-26T09:24:11Z",
      "end": "2026-09-26T09:24:11Z",
      "granularity": "day"
    }
  }
}

GET /v1/analytics/tags #

tags reportanalytics:read

Distinct conversations per tag in the window, with their outcome, ordered by volume. Usage-volume-per-category is deliberately not here: a conversation with two tags of one category counts twice, which reads like a conversation count and is not one. Requires the analytics:read scope.

Query parameters

FieldTypeDescription
start_date string · date

ISO8601 date (YYYY-MM-DD). Defaults to 30 days ago. Anything else is a 400 — a date we guessed at is a window you never asked for.

end_date string · date

ISO8601 date (YYYY-MM-DD). Defaults to today.

granularity string day week month

Bucket size for the series. An unrecognised value falls back to day, and the response says which one it used.

Responses

200the report

Headers X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

FieldTypeDescription
datarequired PublicTagAnalytics
metarequired MetaForPeriod
400a date that is not ISO8601GeneralError
401missing, unknown, revoked or expired credentialGeneralError
403the credential lacks the required scopeGeneralError
429the organization's per-endpoint ceiling was reachedGeneralError

Headers Retry-After X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset

cURL
curl https://api.fellix.ai/v1/analytics/tags \
  -H "Authorization: Bearer $FELLIX_API_KEY"
200 response
{
  "data": {
    "by_tag": [
      {
        "tag": {
          "id": 1,
          "name": "Ada",
          "color": "string",
          "category_key": "string"
        },
        "total": 12,
        "open": 12,
        "won": 12,
        "lost": 12,
        "win_rate": 0.42
      }
    ]
  },
  "meta": {
    "code": "success",
    "status": 200,
    "title": "OK",
    "detail": "Request succeeded",
    "period": {
      "start": "2026-09-26T09:24:11Z",
      "end": "2026-09-26T09:24:11Z",
      "granularity": "day"
    }
  }
}

Schemas

Every shape this API returns, and the payload each webhook carries.

Meta

FieldTypeDescription
code string
status integer
title string
detail string
Example
{
  "code": "success",
  "status": 200,
  "title": "OK",
  "detail": "Request succeeded"
}

ErrorModel

FieldTypeDescription
code string

Error code, "<controller>#<action>#<key>"

status integer

HTTP status repeated in the body

title string

Error title

detail string

Error detail

Example
{
  "code": "not_found",
  "status": 404,
  "title": "Not found",
  "detail": "No such conversation for this organization"
}

GeneralError

FieldTypeDescription
errors ErrorModel[]
Example
{
  "errors": [
    {
      "code": "not_found",
      "status": 404,
      "title": "Not found",
      "detail": "No such conversation for this organization"
    }
  ]
}

MetaForCursorPagination

FieldTypeDescription
coderequired string
statusrequired integer
titlerequired string
detailrequired string
paginationrequired object
next_cursorrequired string nullable

Opaque. Absent on the last page.

has_morerequired boolean
Example
{
  "code": "success",
  "status": 200,
  "title": "OK",
  "detail": "Request succeeded",
  "pagination": {
    "next_cursor": "IjIwMjYtMDktMjZUMDk6MjQ6MTFaIg",
    "has_more": true
  }
}

PublicOrganization

FieldTypeDescription
idrequired integer
namerequired string nullable
time_zonerequired string

IANA name. Timestamps are UTC regardless.

default_locale string nullable
created_atrequired string · date-time
Example
{
  "id": 1,
  "name": "Ada",
  "time_zone": "Europe/Istanbul",
  "default_locale": "en-US",
  "created_at": "2026-09-26T09:24:11Z"
}

PublicAccount

FieldTypeDescription
idrequired integer
namerequired string
credential_typerequired string static oauth
scopesrequired string[]

What this credential may do. A :write scope implies its :read.

prefix string nullable

First 12 characters of a static key. Not a secret.

client_id string nullable

OAuth credentials only.

expires_at string · date-time nullable
last_used_at string · date-time nullable
created_atrequired string · date-time
rate_limits PublicRateLimits

The ceiling in force for THIS credential. requests_per_minute is null when no ceiling applies, in which case no response carries X-RateLimit-* either. A heavier endpoint scales down from this figure, so the number a given endpoint enforces is the X-RateLimit-Limit it returns.

organization PublicOrganization
Example
{
  "id": 1,
  "name": "Ada",
  "credential_type": "static",
  "scopes": [
    "customers:read"
  ],
  "prefix": "flx_live_9f2c",
  "client_id": "flx_cid_4b81",
  "expires_at": "2026-09-26T09:24:11Z",
  "last_used_at": "2026-09-26T09:24:11Z",
  "created_at": "2026-09-26T09:24:11Z",
  "rate_limits": {
    "requests_per_minute": 1,
    "window_seconds": 12,
    "credential_capped": true
  },
  "organization": {
    "id": 1,
    "name": "Ada",
    "time_zone": "Europe/Istanbul",
    "default_locale": "en-US",
    "created_at": "2026-09-26T09:24:11Z"
  }
}

PublicRateLimits

The ceiling in force for THIS credential. requests_per_minute is null when no ceiling applies, in which case no response carries X-RateLimit-* either. A heavier endpoint scales down from this figure, so the number a given endpoint enforces is the X-RateLimit-Limit it returns.

FieldTypeDescription
requests_per_minute integer nullable
window_secondsrequired integer

Length of the sliding window.

credential_capped boolean

True when this credential was held below its organization's ceiling.

Example
{
  "requests_per_minute": 1,
  "window_seconds": 12,
  "credential_capped": true
}

PublicPing

FieldTypeDescription
statusrequired string ok
organization_idrequired integer
timerequired string · date-time

Our clock, UTC. Compare it against yours.

Example
{
  "status": "ok",
  "organization_id": 41,
  "time": "2026-09-26T09:24:11Z"
}

PublicCustomerAccount

FieldTypeDescription
idrequired integer
account_typerequired string

email, phone, instagram, …

detailrequired string

The address, number or handle itself

label string nullable
is_primaryrequired boolean

The one mirrored into the client's flat email/phone

created_atrequired string · date-time
updated_atrequired string · date-time
Example
{
  "id": 1,
  "account_type": "string",
  "detail": "Request succeeded",
  "label": "string",
  "is_primary": false,
  "created_at": "2026-09-26T09:24:11Z",
  "updated_at": "2026-09-26T09:24:11Z"
}

PublicCustomer

FieldTypeDescription
idrequired integer
name string nullable
surname string nullable
email string nullable

The primary email. Every address is in accounts.

phone string nullable

The primary phone. Every number is in accounts.

description string nullable
is_archivedrequired boolean

Archived clients are hidden unless you ask for them

whatsapp_opt_out_at string · date-time nullable

When set, this person asked not to be messaged on WhatsApp. Honour it.

default_locale string nullable
time_zone string nullable
merged_into_id integer nullable

Set when this record was merged into another. Follow it; do not keep writing here.

created_atrequired string · date-time
updated_atrequired string · date-time
accounts?include=accounts PublicCustomerAccount[]

Only present with ?include=accounts

Example
{
  "id": 1,
  "name": "Ada",
  "surname": "Lovelace",
  "email": "ada@example.com",
  "phone": "+15550100",
  "description": "Referred by a colleague",
  "is_archived": false,
  "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
  "default_locale": "en-US",
  "time_zone": "Europe/Istanbul",
  "merged_into_id": 1,
  "created_at": "2026-09-26T09:24:11Z",
  "updated_at": "2026-09-26T09:24:11Z"
}

PublicChannel

FieldTypeDescription
idrequired integer
name string nullable
platformrequired string instagram whatsapp voice facebook webchat
is_activerequired boolean
created_atrequired string · date-time
Example
{
  "id": 1,
  "name": "Ada",
  "platform": "instagram",
  "is_active": false,
  "created_at": "2026-09-26T09:24:11Z"
}

PublicTag

FieldTypeDescription
idrequired integer
namerequired string
color string nullable
category_key string nullable
description string nullable
systemrequired boolean

Maintained by Fellix. Do not assume you can recreate it.

created_atrequired string · date-time
updated_atrequired string · date-time
Example
{
  "id": 1,
  "name": "Ada",
  "color": "string",
  "category_key": "string",
  "description": "Referred by a colleague",
  "system": true,
  "created_at": "2026-09-26T09:24:11Z",
  "updated_at": "2026-09-26T09:24:11Z"
}

PublicMessage

FieldTypeDescription
idrequired integer
conversation_idrequired integer
message_typerequired string text image video audio document location contact poll event reaction sticker reply template interactive system
from_customerrequired boolean

true for inbound, false for anything we sent

content string nullable
language string nullable
external_id string nullable

The provider's own id, for reconciliation

reply_to_id integer nullable
delivery_status string nullable sent delivered read failed pending

Null for an inbound message — delivery is only a question about what we sent. Attachments are not part of v1.

sent_at string · date-time nullable
created_atrequired string · date-time
updated_atrequired string · date-time
conversation?include=conversation PublicConversation nullable

Only present with ?include=conversation

Example
{
  "id": 1,
  "conversation_id": 1,
  "message_type": "text",
  "from_customer": true,
  "content": "On our way.",
  "language": "en",
  "external_id": "msg_9f2c8d41",
  "reply_to_id": 1,
  "delivery_status": "sent",
  "sent_at": "2026-09-26T09:24:11Z",
  "created_at": "2026-09-26T09:24:11Z",
  "updated_at": "2026-09-26T09:24:11Z"
}

PublicConversation

FieldTypeDescription
idrequired integer
customer_id integer nullable
channel_id integer nullable
statusrequired string active completed escalated abandoned blocked
is_archivedrequired boolean
language string nullable
lead_status string nullable open won lost
qualification string nullable hot warm cold
lost_reason string nullable
rating integer nullable
started_at string · date-time nullable
ended_at string · date-time nullable
created_atrequired string · date-time
updated_atrequired string · date-time
customer?include=customer PublicCustomer nullable

Only present with ?include=customer

channel?include=channel PublicChannel nullable

Only present with ?include=channel. Still named after the channel is removed.

tags?include=tags PublicTag[]

Only present with ?include=tags

last_message?include=last_message PublicMessage nullable

Only present with ?include=last_message

Example
{
  "id": 1,
  "customer_id": 1,
  "channel_id": 1,
  "status": "active",
  "is_archived": false,
  "language": "en",
  "lead_status": "open",
  "qualification": "hot",
  "lost_reason": "string",
  "rating": 1,
  "started_at": "2026-09-26T09:24:11Z",
  "ended_at": "2026-09-26T09:24:11Z",
  "created_at": "2026-09-26T09:24:11Z",
  "updated_at": "2026-09-26T09:24:11Z"
}

PublicAppointment

FieldTypeDescription
idrequired integer
customer_id integer nullable
conversation_id integer nullable
channel_id integer nullable
starts_atrequired string · date-time
ends_atrequired string · date-time
statusrequired string requested confirmed cancelled completed no_show
sourcerequired string operator assistant api

Who booked it

notes string nullable
created_atrequired string · date-time
updated_atrequired string · date-time
customer?include=customer PublicCustomer nullable

Only present with ?include=customer

Example
{
  "id": 1,
  "customer_id": 1,
  "conversation_id": 1,
  "channel_id": 1,
  "starts_at": "2026-09-26T09:24:11Z",
  "ends_at": "2026-09-26T09:24:11Z",
  "status": "requested",
  "source": "operator",
  "notes": "Prefers the afternoon",
  "created_at": "2026-09-26T09:24:11Z",
  "updated_at": "2026-09-26T09:24:11Z"
}

PublicUser

FieldTypeDescription
idrequired integer
name string nullable
surname string nullable
email string nullable
Example
{
  "id": 1,
  "name": "Ada",
  "surname": "Lovelace",
  "email": "ada@example.com"
}

PublicLead

FieldTypeDescription
idrequired integer
sourcerequired string meta_lead_ads meta_ctwa meta_ctm meta_organic native_form manual
platform string nullable
statusrequired string new processed failed
external_lead_id string nullable

The provider's own id for the submission

form_id string nullable
form_name string nullable
ad_id string nullable
ad_name string nullable
adset_id string nullable
adset_name string nullable
campaign_id string nullable
campaign_name string nullable
submitted_at string · date-time nullable
customer_id integer nullable
conversation_id integer nullable
created_atrequired string · date-time
updated_atrequired string · date-time
customer PublicCustomer nullable
conversation PublicConversation nullable
Example
{
  "id": 1,
  "source": "meta_lead_ads",
  "platform": "string",
  "status": "new",
  "external_lead_id": "msg_9f2c8d41",
  "form_id": "msg_9f2c8d41",
  "form_name": "string",
  "ad_id": "msg_9f2c8d41",
  "ad_name": "string",
  "adset_id": "msg_9f2c8d41",
  "adset_name": "string",
  "campaign_id": "msg_9f2c8d41",
  "campaign_name": "string",
  "submitted_at": "2026-09-26T09:24:11Z",
  "customer_id": 1,
  "conversation_id": 1,
  "created_at": "2026-09-26T09:24:11Z",
  "updated_at": "2026-09-26T09:24:11Z",
  "customer": {
    "id": 1,
    "name": "Ada",
    "surname": "Lovelace",
    "email": "ada@example.com",
    "phone": "+15550100",
    "description": "Referred by a colleague",
    "is_archived": false,
    "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
    "default_locale": "en-US",
    "time_zone": "Europe/Istanbul",
    "merged_into_id": 1,
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  },
  "conversation": {
    "id": 1,
    "customer_id": 1,
    "channel_id": 1,
    "status": "active",
    "is_archived": false,
    "language": "en",
    "lead_status": "open",
    "qualification": "hot",
    "lost_reason": "string",
    "rating": 1,
    "started_at": "2026-09-26T09:24:11Z",
    "ended_at": "2026-09-26T09:24:11Z",
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  }
}

PublicVoiceCall

FieldTypeDescription
idrequired integer
external_call_id string nullable
statusrequired string pending ringing in_progress completed failed no_answer busy canceled
direction string nullable inbound outbound
from_number string nullable
to_number string nullable
started_at string · date-time nullable
ended_at string · date-time nullable
duration_seconds integer nullable
transcript string nullable

Null until the call ends. The recording itself is not part of v1.

customer_id integer nullable
conversation_id integer nullable
created_atrequired string · date-time
updated_atrequired string · date-time
customer PublicCustomer nullable
conversation PublicConversation nullable
Example
{
  "id": 1,
  "external_call_id": "msg_9f2c8d41",
  "status": "pending",
  "direction": "inbound",
  "from_number": "string",
  "to_number": "string",
  "started_at": "2026-09-26T09:24:11Z",
  "ended_at": "2026-09-26T09:24:11Z",
  "duration_seconds": 12,
  "transcript": "Hello, I would like to move my appointment.",
  "customer_id": 1,
  "conversation_id": 1,
  "created_at": "2026-09-26T09:24:11Z",
  "updated_at": "2026-09-26T09:24:11Z",
  "customer": {
    "id": 1,
    "name": "Ada",
    "surname": "Lovelace",
    "email": "ada@example.com",
    "phone": "+15550100",
    "description": "Referred by a colleague",
    "is_archived": false,
    "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
    "default_locale": "en-US",
    "time_zone": "Europe/Istanbul",
    "merged_into_id": 1,
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  },
  "conversation": {
    "id": 1,
    "customer_id": 1,
    "channel_id": 1,
    "status": "active",
    "is_archived": false,
    "language": "en",
    "lead_status": "open",
    "qualification": "hot",
    "lost_reason": "string",
    "rating": 1,
    "started_at": "2026-09-26T09:24:11Z",
    "ended_at": "2026-09-26T09:24:11Z",
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  }
}

PublicWhatsappCall

FieldTypeDescription
idrequired integer
wacid string nullable

Meta's own call id

statusrequired string ringing completed missed failed
direction string nullable inbound outbound
from_phone string nullable
to_phone string nullable
connected_at string · date-time nullable
started_at string · date-time nullable
ended_at string · date-time nullable
duration_seconds integer nullable
channel_id integer nullable
customer_id integer nullable
conversation_id integer nullable
message_id integer nullable

The timeline row this call is rendered as

created_atrequired string · date-time
updated_atrequired string · date-time
customer PublicCustomer nullable
conversation PublicConversation nullable
channel PublicChannel nullable
message PublicMessage nullable
Example
{
  "id": 1,
  "wacid": "string",
  "status": "ringing",
  "direction": "inbound",
  "from_phone": "string",
  "to_phone": "string",
  "connected_at": "2026-09-26T09:24:11Z",
  "started_at": "2026-09-26T09:24:11Z",
  "ended_at": "2026-09-26T09:24:11Z",
  "duration_seconds": 12,
  "channel_id": 1,
  "customer_id": 1,
  "conversation_id": 1,
  "message_id": 1,
  "created_at": "2026-09-26T09:24:11Z",
  "updated_at": "2026-09-26T09:24:11Z",
  "customer": {
    "id": 1,
    "name": "Ada",
    "surname": "Lovelace",
    "email": "ada@example.com",
    "phone": "+15550100",
    "description": "Referred by a colleague",
    "is_archived": false,
    "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
    "default_locale": "en-US",
    "time_zone": "Europe/Istanbul",
    "merged_into_id": 1,
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  },
  "conversation": {
    "id": 1,
    "customer_id": 1,
    "channel_id": 1,
    "status": "active",
    "is_archived": false,
    "language": "en",
    "lead_status": "open",
    "qualification": "hot",
    "lost_reason": "string",
    "rating": 1,
    "started_at": "2026-09-26T09:24:11Z",
    "ended_at": "2026-09-26T09:24:11Z",
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  },
  "channel": {
    "id": 1,
    "name": "Ada",
    "platform": "instagram",
    "is_active": false,
    "created_at": "2026-09-26T09:24:11Z"
  },
  "message": {
    "id": 1,
    "conversation_id": 1,
    "message_type": "text",
    "from_customer": true,
    "content": "On our way.",
    "language": "en",
    "external_id": "msg_9f2c8d41",
    "reply_to_id": 1,
    "delivery_status": "sent",
    "sent_at": "2026-09-26T09:24:11Z",
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z"
  }
}

PublicWhatsappCallForward

FieldTypeDescription
idrequired integer
target_kind string nullable

What the call was forwarded to

dial_call_status string nullable

The outcome of the attempt

from_phone string nullable
caller_id string nullable
destination_number string nullable
duration_seconds integer nullable
dialed_at string · date-time nullable
completed_at string · date-time nullable
channel_id integer nullable
whatsapp_call_id integer nullable
created_atrequired string · date-time
updated_atrequired string · date-time
channel PublicChannel nullable
whatsapp_call PublicWhatsappCall nullable

Null while the link to the originating call has not resolved, which is a normal state

Example
{
  "id": 1,
  "target_kind": "string",
  "dial_call_status": "string",
  "from_phone": "string",
  "caller_id": "msg_9f2c8d41",
  "destination_number": "string",
  "duration_seconds": 12,
  "dialed_at": "2026-09-26T09:24:11Z",
  "completed_at": "2026-09-26T09:24:11Z",
  "channel_id": 1,
  "whatsapp_call_id": 1,
  "created_at": "2026-09-26T09:24:11Z",
  "updated_at": "2026-09-26T09:24:11Z",
  "channel": {
    "id": 1,
    "name": "Ada",
    "platform": "instagram",
    "is_active": false,
    "created_at": "2026-09-26T09:24:11Z"
  },
  "whatsapp_call": {
    "id": 1,
    "wacid": "string",
    "status": "ringing",
    "direction": "inbound",
    "from_phone": "string",
    "to_phone": "string",
    "connected_at": "2026-09-26T09:24:11Z",
    "started_at": "2026-09-26T09:24:11Z",
    "ended_at": "2026-09-26T09:24:11Z",
    "duration_seconds": 12,
    "channel_id": 1,
    "customer_id": 1,
    "conversation_id": 1,
    "message_id": 1,
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z",
    "customer": {
      "id": 1,
      "name": "Ada",
      "surname": "Lovelace",
      "email": "ada@example.com",
      "phone": "+15550100",
      "description": "Referred by a colleague",
      "is_archived": false,
      "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
      "default_locale": "en-US",
      "time_zone": "Europe/Istanbul",
      "merged_into_id": 1,
      "created_at": "2026-09-26T09:24:11Z",
      "updated_at": "2026-09-26T09:24:11Z"
    },
    "conversation": {
      "id": 1,
      "customer_id": 1,
      "channel_id": 1,
      "status": "active",
      "is_archived": false,
      "language": "en",
      "lead_status": "open",
      "qualification": "hot",
      "lost_reason": "string",
      "rating": 1,
      "started_at": "2026-09-26T09:24:11Z",
      "ended_at": "2026-09-26T09:24:11Z",
      "created_at": "2026-09-26T09:24:11Z",
      "updated_at": "2026-09-26T09:24:11Z"
    },
    "channel": {
      "id": 1,
      "name": "Ada",
      "platform": "instagram",
      "is_active": false,
      "created_at": "2026-09-26T09:24:11Z"
    },
    "message": {
      "id": 1,
      "conversation_id": 1,
      "message_type": "text",
      "from_customer": true,
      "content": "On our way.",
      "language": "en",
      "external_id": "msg_9f2c8d41",
      "reply_to_id": 1,
      "delivery_status": "sent",
      "sent_at": "2026-09-26T09:24:11Z",
      "created_at": "2026-09-26T09:24:11Z",
      "updated_at": "2026-09-26T09:24:11Z"
    }
  }
}

PublicTeam

FieldTypeDescription
idrequired integer
namerequired string
total_members integer
created_atrequired string · date-time
updated_atrequired string · date-time
channels PublicChannel[]
Example
{
  "id": 1,
  "name": "Ada",
  "total_members": 12,
  "created_at": "2026-09-26T09:24:11Z",
  "updated_at": "2026-09-26T09:24:11Z",
  "channels": [
    {
      "id": 1,
      "name": "Ada",
      "platform": "instagram",
      "is_active": false,
      "created_at": "2026-09-26T09:24:11Z"
    }
  ]
}

PublicTeamMember

FieldTypeDescription
idrequired integer
team_idrequired integer
user_idrequired integer
created_atrequired string · date-time
updated_atrequired string · date-time
team PublicTeam nullable
user PublicUser nullable
Example
{
  "id": 1,
  "team_id": 1,
  "user_id": 1,
  "created_at": "2026-09-26T09:24:11Z",
  "updated_at": "2026-09-26T09:24:11Z",
  "team": {
    "id": 1,
    "name": "Ada",
    "total_members": 12,
    "created_at": "2026-09-26T09:24:11Z",
    "updated_at": "2026-09-26T09:24:11Z",
    "channels": [
      {
        "id": 1,
        "name": "Ada",
        "platform": "instagram",
        "is_active": false,
        "created_at": "2026-09-26T09:24:11Z"
      }
    ]
  },
  "user": {
    "id": 1,
    "name": "Ada",
    "surname": "Lovelace",
    "email": "ada@example.com"
  }
}

WebhookEnvelope

FieldTypeDescription
idrequired string

Stable across endpoints AND redeliveries — dedupe on it

typerequired string

The event name, e.g. conversation.created

api_versionrequired string
created_atrequired string · date-time

When the event happened

organization_idrequired integer
datarequired object
typerequired string

The resource, e.g. conversation

idrequired integer
attributesrequired object

The resource, in exactly the shape a GET of it returns. Reflects the record at payload-build time, not at delivery time.

metrics object nullable

Present on metric-sourced events

Example
{
  "id": "evt_9f2c8d41b7e3",
  "type": "conversation.created",
  "api_version": "v1",
  "created_at": "2026-09-26T09:24:11Z",
  "organization_id": 41,
  "data": {
    "type": "conversation",
    "id": 1,
    "attributes": {},
    "metrics": {}
  }
}

PublicCustomerInput

FieldTypeDescription
customerrequired object
namerequired string
surname string nullable
email string nullable

Unique within the organization

phone string nullable

Unique within the organization

description string nullable
is_archived boolean
whatsapp_opt_out_at string · date-time nullable

Set it to record a refusal you took yourself; clear it only on an explicit request from the person.

default_locale string nullable
time_zone string nullable

IANA name

Example
{
  "customer": {
    "name": "Ada",
    "surname": "Lovelace",
    "email": "ada@example.com",
    "phone": "+15550100",
    "description": "Referred by a colleague",
    "is_archived": false,
    "whatsapp_opt_out_at": "2026-09-26T09:24:11Z",
    "default_locale": "en-US",
    "time_zone": "Europe/Istanbul"
  }
}

PublicAppointmentInput

FieldTypeDescription
appointmentrequired object
customer_idrequired integer
conversation_id integer nullable
channel_id integer nullable
starts_atrequired string · date-time
ends_at string · date-time nullable

Omit and it becomes one slot of the organization's configured length. Moving starts_at without an end keeps the same duration.

status string requested confirmed cancelled completed no_show

Cancel by setting cancelled. There is no DELETE — a cancelled booking is kept.

notes string nullable
Example
{
  "appointment": {
    "customer_id": 1,
    "conversation_id": 1,
    "channel_id": 1,
    "starts_at": "2026-09-26T09:24:11Z",
    "ends_at": "2026-09-26T09:24:11Z",
    "status": "requested",
    "notes": "Prefers the afternoon"
  }
}

PublicMessageInput

FieldTypeDescription
messagerequired object
content string

Length limits are the platform's: 4096 on WhatsApp, 2000 on Instagram.

file string · binary

Send as multipart/form-data. Website chat channels only — the other platforms' send paths carry text, so a file accepted there would be stored and never delivered.

Example
{
  "message": {
    "content": "On our way.",
    "file": "<binary>"
  }
}

PublicTemplateInput

FieldTypeDescription
templaterequired object
namerequired string

The approved template name as WhatsApp knows it

languagerequired string

e.g. en_US

components object[]

Body/header parameters, in Meta's own component shape

Example
{
  "template": {
    "name": "Ada",
    "language": "en",
    "components": [
      {}
    ]
  }
}

MetaForPeriod

FieldTypeDescription
coderequired string
statusrequired integer
titlerequired string
detailrequired string
periodrequired object

The window this answer is for. Always present, because a figure without its window is not a figure.

startrequired string · date-time
endrequired string · date-time
granularityrequired string day week month
Example
{
  "code": "success",
  "status": 200,
  "title": "OK",
  "detail": "Request succeeded",
  "period": {
    "start": "2026-09-26T09:24:11Z",
    "end": "2026-09-26T09:24:11Z",
    "granularity": "day"
  }
}

PublicPipelineAnalytics

FieldTypeDescription
openrequired integer

Conversations sitting at lead_status: open

wonrequired integer
lostrequired integer
win_raterequired number

Percentage of settled conversations that were won

avg_days_to_won number nullable
avg_days_to_lost number nullable
by_lost_reason object

Keyed by your own lost-reason keys, not by anything we define

by_qualification object

Keyed by qualification (hot/warm/cold)

Example
{
  "open": 12,
  "won": 12,
  "lost": 12,
  "win_rate": 0.42,
  "avg_days_to_won": 3.5,
  "avg_days_to_lost": 3.5,
  "by_lost_reason": {},
  "by_qualification": {}
}

PublicTrendAnalytics

FieldTypeDescription
granularityrequired string day week month

The one actually used — echoed because an unrecognised request falls back

pointsrequired object[]
Example
{
  "granularity": "day",
  "points": [
    {
      "period": "2026-09-26T09:24:11Z",
      "won": 12,
      "lost": 12
    }
  ]
}

PublicTagAnalytics

FieldTypeDescription
by_tagrequired object[]

Distinct conversations per tag in the window, ordered by volume.

Example
{
  "by_tag": [
    {
      "tag": {
        "id": 1,
        "name": "Ada",
        "color": "string",
        "category_key": "string"
      },
      "total": 12,
      "open": 12,
      "won": 12,
      "lost": 12,
      "win_rate": 0.42
    }
  ]
}

OAuthToken

FieldTypeDescription
access_tokenrequired string
token_typerequired string Bearer
expires_inrequired integer

Seconds.

scope string

Space-separated, as RFC 6749 requires.

Example
{
  "access_token": "flx_at_9f2c8d41b7e34a6f",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "customers:read conversations:read"
}

OAuthError

FieldTypeDescription
errorrequired string invalid_client unsupported_grant_type
error_description string
Example
{
  "error": "invalid_client",
  "error_description": "string"
}