# Overview

An overview of the Circle Developer platform

<figure><img src="/files/gtsnvrHElXe6bPjzvRR2" alt=""><figcaption></figcaption></figure>

Circle provides a powerful suite of APIs to developers to build automations, run migrations, bring data into your own data warehouse, or natively integrate community features into your own website or app.

## The Circle Developer Platform

#### **1.** [**Admin API**](/apis/admin-api)

Available to customers on our Business plan and above, our [**Admin API**](/apis/admin-api) is designed for community admins to build automations, migration scripts, and administrative integrations.

Requests are admin-authenticated, which means the API can only be used to build administrative tooling and automations.

#### **2.** [**Headless**](/apis/headless)

Available to customers on our Business plan and above, our **Headless** offering is designed for communities to integrate Circle features into their own website or app, like discussions, feed, notifications, events, and more.

Headless consists of three components:

* [**Member API**](/apis/headless/member-api): A server-side API with endpoints for building your own member-side experiences in your app or website. [See endpoints](https://api-headless.circle.so/?urls.primaryName=Member%20APIs)\
  \
  Unlike the admin API, requests are member-authenticated via the member-specific JWT tokens you'll generate with the [Auth API](https://api-headless.circle.so/).\
  \
  This means every API request is made on behalf of a signed in member on your website or app, allowing you to write your own client-side code for integrating posts, comments, events, notifications, and more into your website or app.\ <br>
* [**Auth API**](https://api.circle.so/apis/headless/quick-start)**:** A server-side API to authenticate your website or app’s signed in members with the Member API using a JWT token. [See endpoints](https://api-headless.circle.so/)\
  \
  Optionally, you can use the [Auth SDK ](/apis/headless/auth-sdk)for Node.js to get a head-start with your Node application.

#### **3.** [**Data API**](/apis/data-api)

Available to customers on our Plus Platform plan only, the [**Data API**](/apis/data-api) is a special API which lets your data team integrate your community's event stream data into your own warehouse via an ETL integration with a tool like [Airbyte](https://airbyte.com/?utm_term=airbyte\&utm_campaign=TDD_Search_Brand_USA\&utm_source=adwords\&utm_medium=ppc\&hsa_acc=4651612872\&hsa_cam=20643300404\&hsa_grp=155311392438\&hsa_ad=676665180945\&hsa_src=g\&hsa_tgt=kwd-1354459259989\&hsa_kw=airbyte\&hsa_mt=p\&hsa_net=adwords\&hsa_ver=3\&gad_source=1\&gclid=Cj0KCQjw0Oq2BhCCARIsAA5hubW5f6DBG_s2vIImSEgivxfKG6cwVCBAIMsJbJ5igqURirIbhe5elq8aAtFcEALw_wcB) or similar.

### Feedback

* If you have general questions or want to share your creations with the developer community, please check out our [Developer community space](https://community.circle.so/c/developers/).&#x20;
* If you have API feedback for our engineering team, [please use this form](https://circleco.typeform.com/to/xFEpyITZ#email=xxxxx\&visitor=xxxxx) to reach out to us.<br>

## More information

If you're new to Circle, we recommend checking out the [Knowledge Base](https://help.circle.so/p/basics).


# Concepts


# Spaces & space groups

Spaces are a foundational container for discussions, courses, events, chat, and image posts in Circle.

A space is both a container of content, and the access gating mechanism through which members access this content.

### Spaces

#### Access

* **Public**: The sidebar link is visible to everyone in the community, and the space is accessible to all community members.
* **Private**: The sidebar link is visible to members in the community, but the space is accessible only to members who've been added to it.
* **Secret**: The sidebar link is hidden from members in the community unless a member belongs to the space, and the space is accessible only to members who've been added to it.

#### Type

Each space is defined by its **space type**, which defines its primary content and functionality. Here's an example of a TypeScript definition:

```typescript
type SpaceType = 'basic' | 'chat' | 'event' | 'course' | 'members' | 'image';
```

* **Basic**:
  * Contains regular posts, serving as a general-purpose space for posts and discussions.
  * Use cases: General discussions, announcements, topic-specific conversations, or blog-like content.
* **Chat**:
  * Functions as a chat room, allowing multiple participants to engage in real-time conversations.
  * Use cases: Real-time support, casual conversations, quick team check-ins, or live Q\&A sessions.
* **Event**:
  * Functions as a container for upcoming and past events.
  * Use cases: Webinars, meetups, conference planning, or virtual event hosting.
* **Course**:
  * Functions as a container on course lessons.
  * Use cases: Online classes, training programs, tutorial series, or self-paced learning modules.
* **Members**:
  * Functions as a container for a specific member directory.
  * Use cases: Networking, member spotlights, expertise directories, or team rosters.
* **Image**:
  * Functions as a container for images.
  * Use cases: Photo galleries, visual portfolios, product showcases, or inspirational mood boards.

## Space groups

Space groups are the grouping container of multiple spaces in the sidebar, and include meta settings. A Space Group can contain spaces of different types and access levels.


# Posts

An overview of posts on Circle

### Post types

There are three types of posts in Circle spaces:

* **Basic**:
  * A post in a post space.
  * Supports text, links, images, videos, embedded media and much more.
  * Ideal for general discussions, announcements, or longer-form content.
* **Event**:
  * An event in an event space.
  * Includes a sophisticated but simple body and meta information
  * Optimal for visual storytelling, portfolios, or product showcases.
* **Image:**
  * A image in an image space.
  * Supports single or multiple images.
  * Optimal for visual storytelling, portfolios, or product showcases.

### Basic posts

Here's an example of the structure of a **basic** post in a posts space:

```typescript
{
  post_type: 'basic',
  space_type: 'basic',
  id: number,
  name: string,
  display_title: string,
  slug: string,
  body: {
    attachments: {},
    html: string,
  },
  body_plain_text: string,
  url: string,
  created_at: string,
  updated_at: string,
  space: {
    id: number,
    slug: string,
    name: string,
    emoji: string,
  },
  author: {
    id: number,
    name: string,
    avatar_url: string,
    roles: string[],
  },
  tiptap_body: TipTapRichTextBody
  // ... other attributes
}
```

### Image posts

Image posts are similar to basic posts, but include a **gallery** attribute:

* **gallery**: An array of images, each containing (along side other attributes):
  * Original image URL
  * Optimized image URL (for improved performance)
  * Dimensions (width and height)

```typescript
gallery: {
  id: number
  downloadable_images: boolean
  images: {
    id: number
    signed_id: string
    original_url: string
    url: string
    filename: string
    width: number
    height: number
  }[]
}
```

Galleries supports multiple images per post, and images are optimized automatically upon upload. An image may include additional metadata such dimensions, file size, EXIF data.

### Event posts

Event posts are designed for managing and displaying events in an event space.

In addition to the output from a **basic** post, you'll find event-specific attributes such as:

```typescript
{
  tiptap_body: TipTapRichTextBody
  event_attendees?: {
    count: number;
    records: User[];
  };
  event_settings_attributes: {
    starts_at: string;
    ends_at: string;
    in_person_location: string | null;
    hide_location_from_non_attendees: boolean;
    duration_in_seconds: number;
    rsvp_disabled: boolean;
    hide_attendees: boolean;
    send_email_reminder: boolean;
    send_in_app_notification_reminder: boolean;
    send_email_confirmation: boolean;
    send_in_app_notification_confirmation: boolean;
    enable_custom_thank_you_message: boolean;
    confirmation_message_title: string | null;
    confirmation_message_description: string | null;
    confirmation_message_button_title: string | null;
    confirmation_message_button_link: string | null;
    location_type: LocationType;
    virtual_location_url: string | null;
    live_stream_recording_url?: string | null;
    rsvp_limit?: number | null;
    rsvp_count: number;
  };
  event_recurring_settings_attributes: {
    frequency: 'daily' | 'weekly' | 'bi_weekly' | 'monthly' | 'monthly_weekday_based' | 'annually';
    ends_at: string;
    occurrences: string | null;
    range_type: string;
  };
  paywall_attributes?: {
    amount: number
    checkout_url: string
    currency_code: string
    currency_symbol: string
  }
  // ... other post attributes
}
```

\ <br>


# Messages

Circle supports messaging via direct messages, group chat rooms, and chat spaces.

### Chat Rooms

A Chat Room is the shared container for all real-time chat conversations in Circle.

#### Kind

There are two kinds of Chat Rooms:

```typescript
type ChatRoomKind = 'direct' | 'group_chat'
```

* **direct** - for one-on-one conversations
* **group\_chat** - for rooms with multiple people

#### Embedded rooms

A Chat Room can be embedded in a course lesson, live room, or a space:

* **Course Lesson**:&#x20;
  * When a Chat Room belongs to a Lesson for course comments, you’ll see an additional `course_lesson` attribute denoting the lesson ID.
* **Live Room**:&#x20;
  * When a Chat Room belongs to a Live Room for chat in a live stream, you’ll see an additional `parent` attribute denoting the live room ID.
* **Space**:&#x20;
  * When a Chat Room belongs to a Space as a chat space, you’ll see an additional `parent` attribute denoting the live room ID.

#### Structure

```typescript
interface ChatRoom {
  id: number;
  uuid: string;
  community_id: number;
  identifier: string;
  kind: 'direct' | 'group_chat';
  name: string;
  description: string | null;
  is_embedded: boolean;
  show_history: boolean;
  last_message_id: number | null;
  pinned_message_id: number | null;
  parent_id: number | null;
  parent_type: string | null;
  created_at: Date;
  updated_at: Date;
  deleted_at: Date | null;
  community: Community;
  // available when part of a Course Lesson
  course_lesson?: CourseLesson;
  chat_room_messages: ChatRoomMessage[];
  chat_threads: ChatThread[];
  chat_room_participants: ChatRoomParticipant[];
  pinned_chat_rooms: PinnedChatRoom[];
  community_members: CommunityMember[];
  reported_participants: ReportedParticipant[];
  direct_chat_room_identifier?: DirectChatRoomIdentifier;
  last_message?: ChatRoomMessage;
  pinned_message?: ChatRoomMessage;
  // available only when the room is inside a LiveRoom or a Space (chat space)
  parent?: Space | LiveRoom;
}
```

### Chat Room Message

A ChatRoomMessage represents a single message within a chat room. It is always attached to a ChatRoom and a participant.

#### Threads/Replies

A message can contain **replies**, or be a **reply** to another message. When a message contains replies, this is called a Thread.

* The attribute `parentMessageId` tells you if that message you are looking at is part of a thread.
* The attribute `hasReplies` tells you if that message has any nested replies.

#### Structure

```typescript
interface ChatRoomMessage {
  id: number
  chat_room_id: number
  chat_room_uuid: string
  chat_room_participant_id: number
  body: string
  bookmark_id: number | null
  // uses TipTap, similar to Posts but a simplified version
  rich_text_body?: TipTapRichBodyText
  sent_at: string
  created_at: string
  updated_at: string | null
  deleted_at: string | null
  edited_at?: string | null
  embedded: false
  chat_thread_id?: number
  reactions: ChatMessageReaction[]
  creation_uuid?: string
  sender: {
    id: number
    name: string
    community_member_id: number
    user_public_uid: string
    avatar_url: string
  }
  thread_participant_avatar_urls: string[]
  parent_message_id: number | null
  replies_count: number
  total_thread_participants_count: number
  thread_participants_preview?: OtherParticipant[]
  chat_thread_replies_count: number
}

type ReactionEmoji = 'thumbsup' | 'heart' | 'joy' | 'open_mouth' | 'cry' | 'pray' | 'tada'

type ChatMessageReaction = {
  emoji: ReactionEmoji
  count: number
  community_member_ids: number[]
}
```


# TipTap editor

Circle has chosen TipTap as the foundation for its text editor, used across posts, comments, and messages. You will usually find it under `tiptap_body` property inside posts or as the content of a message.

### Blocks

TipTap's foundation is based on the concept of **blocks.** A block a self-contained unit of content that has it's own structure, formatting, and functionality.&#x20;

**Examples**

* A paragraph of text
* An image
* A video embed
* A quote or testimonial
* A list (ordered or unordered)
* A custom interactive element (like a poll)

### Available blocks

Circle has a few TipTap blocks ready to be used out-of-the-box.

```typescript
{
  type:
    | 'doc'
    | 'paragraph'
    | 'heading'
    | 'blockquote'
    | 'orderedList'
    | 'bulletList'
    | 'image'
    | 'text'
    | 'hardBreak'
    | 'mention'
    | 'listItem'
    | 'embed'
    | 'codeBlock'
    | 'horizontalRule'
    | 'file'
    | 'entity'
    | 'poll'
}
```

* **Paragraph**<br>

  ```json
  {
    "type": "paragraph",
    "content": [
        {
            "text": "This is a paragraph",
            "type": "text",
            "circle_ios_fallback_text": "This is a paragraph"
        }
    ]
  }
  ```
* **Heading level 2**<br>

  ```json
  {
    "type": "heading",
    "attrs": {
        "level": 2
    },
    "content": [
        {
            "text": "This is 2nd level heading",
            "type": "text",
            "circle_ios_fallback_text": "This is 2nd level heading"
        }
    ]
  }
  ```
* **Heading level 3**<br>

  ```json
  {
    "type": "heading",
    "attrs": {
        "level": 3
    },
    "content": [
        {
            "text": "This is 3rd level heading",
            "type": "text",
            "circle_ios_fallback_text": "This is 2nd level heading"
        }
    ]
  }
  ```
* **Bulleted list**<br>

  ```json
  {
    "type": "bulletList",
    "content": [
        {
            "type": "listItem",
            "content": [
                {
                    "type": "paragraph",
                    "content": [
                        {
                            "text": "This is a bullet list and this is item 1",
                            "type": "text"
                        }
                    ]
                }
            ]
        },
        {
            "type": "listItem",
            "content": [
                {
                    "type": "paragraph",
                    "content": [
                        {
                            "text": "This is item 2",
                            "type": "text"
                        }
                    ]
                }
            ]
        }
    ]
  }
  ```
* **Numbered list**<br>

  ```json
  {
    "type": "orderedList",
    "attrs": {
        "start": 1
    },
    "content": [
        {
            "type": "listItem",
            "content": [
                {
                    "type": "paragraph",
                    "content": [
                        {
                            "text": "This is a number list and this is item 1",
                            "type": "text"
                        }
                    ]
                }
            ]
        },
        {
            "type": "listItem",
            "content": [
                {
                    "type": "paragraph",
                    "content": [
                        {
                            "text": "This is item 2 in the list",
                            "type": "text"
                        }
                    ]
                }
            ]
        }
    ]
  }
  ```
* **Blockquote**<br>

  ```json
  {
    "type": "blockquote",
    "content": [
        {
            "type": "paragraph",
            "content": [
                {
                    "text": "This is a blockquote",
                    "type": "text"
                }
            ]
        }
    ]
  }
  ```
* **Embeds (Youtube, Vimeo, Wistia etc)**<br>

  ```json
  {
    "type": "embed",
    "attrs": {
        "sgid": "BAh7CEkiCGdpZAY6BkVUSSI1Z2lkOi8vanVtcHN0YXJ0LWFwcC9SaWNoVGV4dE9lbWJlZC81MT9leHBpcmVzX2luBjsAVEkiDHB1cnBvc2UGOwBUSSIUcmljaF90ZXh0X2ZpZWxkBjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==--8c4d9e538b2a988746e77e96dcf048616db6962d"
    }
  }
  ```

  Metadata required to render will be sent in `sgid_to_object_map`
* **Image embeds**<br>

  ```json
  {
    "type": "image",
    "attrs": {
        "url": "<http://test.circledev.net:5000/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBZnM9IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--3dd03246287b040de40d07d9e81a57cc9baa1b4c/JgOeRuGD_Y4>",
        "width": "100%",
        "alignment": "center", // enum ["center"|"left"|"right"]
        "signed_id": "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBZnM9IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--3dd03246287b040de40d07d9e81a57cc9baa1b4c",
        "content_type": "image/jpeg"
    }
  }
  ```

  Metadata required to render will be sent in `inline_attachments`&#x20;
* **CodeBlocks**<br>

  ```json
  {
    "type": "codeBlock",
    "attrs": {
       "language": "javascript"
    },
    "content":[
       {
         "text":"console.log('Hey!');",
         "type":"text",
         "circle_ios_fallback_text":"console.log('Hey!');"
       }
    ]
  }
  ```
* **Horizontal Rule**

  ```json
  {
    "type": "horizontalRule",
  }
  ```
* **Mention (inside a paragraph)**

  ```json
  {
     "type":"paragraph",
     "content":[
        {
           "type":"text",
           "text":""
        },
        {
           "type":"mention",
           "attrs":{
              "sgid":"some-sgid"
           },
           "circle_ios_fallback_text":"@John Doe"
        },
        {
           "type":"text",
           "text":" and some text"
        }
     ]
  }
  ```
* More to come

## Understanding SGIDs in TipTap Blocks

### What is SGID?

The **sgid** (Signed Global ID) attribute is a crucial component in TipTap blocks as it identifies elements within a block, such as an image, a file or even an user mention.

It acts as a pointer to specific metadata within the block, so the `sgid` value corresponds to an entry in the `sgids_to_object_map`, where the more detailed metadata is present

### How It Works

1. Within a TipTap block, you'll find an `attrs.sgid` value.
2. To retrieve the associated metadata:
   * Look up this `sgid` value in the `sgids_to_object_map` object.
   * The corresponding entry provides detailed information about the referenced item.

### Example Usage

```javascript
javascriptCopy// TipTap block with an sgid
const block = {
  type: "image",
  attrs: {
    sgid: "BAh7CEkiCGdpZAY6BkVUSSIrZ2lkOi8vY2lyY2xlL0F0dGFjaG1lbnQvMTIzP2V4cGlyZXNfaW4GOwBUSSIMcHVycG9zZQY7AFRJIg9hdHRhY2hhYmxlBjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==",
    // other attributes...
  }
};

// Corresponding entry in sgids_to_object_map
const sgids_to_object_map = {
  "BAh7CEkiCGdpZAY6BkVUSSIrZ2lkOi8vY2lyY2xlL0F0dGFjaG1lbnQvMTIzP2V4cGlyZXNfaW4GOwBUSSIMcHVycG9zZQY7AFRJIg9hdHRhY2hhYmxlBjsAVEkiD2V4cGlyZXNfYXQGOwBUMA==": {
    id: 123,
    filename: "example.jpg",
    content_type: "image/jpeg",
    // other metadata...
  }
};

// Retrieving metadata
const metadata = sgids_to_object_map[block.attrs.sgid];
console.log(metadata); // Outputs the detailed information about the image
```


# File uploads

Here's how **file uploads** work with endpoints that accept file parameters:

1. The client makes an API call to Circle's API, sending file information.
2. The server requests a pre-signed URL from our storage based on the file information received.
3. The storage returns the pre-signed URL to the server.
4. The server returns the pre-signed URL to the client as a response to the initial API call.
5. The client uses this URL to upload the file directly to the storage, which sends a confirmation to the client.

<figure><img src="/files/lYcSS18Q5BGJY17zlnmx" alt=""><figcaption></figcaption></figure>

#### Request

To perform the first API call to the Member API, you need to mount the request correctly.&#x20;

```bash
curl -X POST 'https://app.circle.so/api/headless/v1/direct_uploads' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-d '{
  "blob": {
    "key": "your_file_key",
    "filename": "your_filename.ext",
    "content_type": "your_content_type",
    "byte_size": 12345,
    "checksum": "your_file_checksum"
  }
}'
```

The `checksum` attribute is a Base64 version of your file MD5. Here's a TypeScript sample of how mounting those files in a browser will look like.

```typescript
const convertFileToMd5 = (file: File): Promise<string> => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onload = function (event) {
      const binary = event.target?.result;
      if (binary) {
        const wordArray = enc.Latin1.parse(binary as string);
        const md5 = MD5(wordArray).toString();
        resolve(md5);
      } else {
        reject(new Error('Failed to read file'));
      }
    };

    reader.onerror = function (error) {
      reject(error);
    };

    reader.readAsBinaryString(file);
  });
};

const mountFileToSend = (file: File, md5: string) => {
  const base64 = btoa(
    md5
      .match(/\w{2}/g)!
      .map((a) => String.fromCharCode(parseInt(a, 16)))
      .join('')
  );
  return {
    key: '',
    filename: file.name,
    byte_size: file.size,
    checksum: base64,
    content_type: file.type,
    size: file.size,
  };
};

// usage
const md5 = await convertFileToMd5(file);
// this is what will be sent to the API
const fileData = mountFileToSend(file, md5);
```

#### Response

The important key here is `direct_upload`, that's where the signed url that the file will be sent to is, it's also the place where the headers live, those will also be sent to the same URL as part of the PUT request.&#x20;

```json
{
  "id": 12345,
  "key": "file_key_123",
  "filename": "example.jpg",
  "content_type": "image/jpeg",
  "byte_size": 1048576,
  "checksum": "abcdef1234567890",
  "created_at": "2024-09-12T14:30:00Z",
  "metadata": {
    "identified": true
  },
  "service_name": "s3",
  "signed_id": "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--abcdef1234567890",
  "attachable_sgid": "BAh7CEkiCGdpZAY6BkVUSSIpZ2lkOi8vbXktYXBwL0Jsb2IvMTIzNDU2Nzg5MAY7AFRJIgxwdXJwb3NlBjsAVEkiD2F0dGFjaGFibGUGOwBUSSIPZXhwaXJlc19hdAY7AFQw",
  "direct_upload": {
    "url": "https://your-bucket.s3.amazonaws.com/uploads/123456789",
    "headers": {
      "Content-Type": "image/jpeg",
      "Content-MD5": "abcdef1234567890="
    }
  },
  "url": "https://your-cdn.com/uploads/123456789/example.jpg"
}
```

here's an example on how the request would look like in TypeScript.

```typescript
fetch(direct_upload.url, {
        method: 'PUT',
        headers: direct_upload.headers as Record<
          string,
          string
        >,
        // the same file we generated the MD5 from, you can send the File itself
        body: file,
      });
```


# Rich Text Body

## Rich Text Body Format Documentation

For certain API endpoints like creating a chat room message API, we expect a message body in `rich_text_body`.

This document explains how to structure rich text content in the API, including text, links, mentions, and attachments.

### Basic Structure

The rich text body follows a nested JSON structure with a root object containing a `body` field:

```json
{
  "body": {
    "type": "doc",
    "content": [
      // Content nodes go here
    ]
  }
}
```

### Content Types

#### 1. Plain Text

To include plain text, use a paragraph node with text content:

```json
{
  "type": "paragraph",
  "content": [
    {
      "type": "text",
      "text": "Your message here"
    }
  ]
}
```

#### 2. Links

To add a link, include the link attributes in the `marks` array:

```json
{
  "type": "text",
  "marks": [
    {
      "type": "link",
      "attrs": {
        "href": "https://www.circle.so",
        "target": "_blank"
      }
    }
  ],
  "text": "https://www.example.com"
}
```

#### 3. Mentions

To mention a user, use the mention type with their SGID:

```json
{
  "type": "mention",
  "attrs": {
    "sgid": "BAh7CEkiCGdpZAY6BkVUSSI7Z2lkOi8v..." // User's SGID
  }
}
```

#### 4. Line Breaks

To add a line break, use the hardBreak type:

```json
{
  "type": "hardBreak"
}
```

#### 5. Attachments

To include attachments, add an `attachments` array at the root level with signed IDs:

```json
{
  "body": {
    "type": "doc",
    "content": [
      // Content nodes
    ]
  },
  "attachments": [
    "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaH..." // Signed ID from direct upload
  ]
}
```

### Complete Example

Here's a complete example that combines multiple elements:

```json
{
  "body": {
    "type": "doc",
    "content": [
      {
        "type": "paragraph",
        "content": [
          {
            "type": "text",
            "text": "Hello "
          },
          {
            "type": "mention",
            "attrs": {
              "sgid": "BAh7CEkiCGdpZAY6BkVUSSI7Z2lkOi8v..."
            }
          },
          {
            "type": "text",
            "text": ", please check "
          },
          {
            "type": "text",
            "marks": [
              {
                "type": "link",
                "attrs": {
                  "href": "https://www.circle.so",
                  "target": "_blank"
                }
              }
            ],
            "text": "this link"
          }
        ]
      }
    ]
  },
  "attachments": [
    "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaH..."
  ]
}
```

### Important Notes

1. All content must be wrapped in a paragraph node
2. Links require both `href` and `target` attributes
3. Mentions require a valid SGID
4. Attachments must be uploaded separately first to obtain signed IDs
5. Line breaks can be added between any content nodes using `hardBreak` type

### Tips for Implementation

1. Always validate the JSON structure before sending
2. Ensure all SGIDs for mentions are valid and current
3. Verify that attachment signed IDs are obtained from the direct upload endpoint
4. Test the content rendering with various combinations of elements
5. Handle line breaks appropriately for proper message formatting


# Websockets (Beta)

Websockets are a crucial part of any real-time feature. On our case, we're talking about Chat and Notifications.

## WebSockets Connection Test

This guide explains how to test WebSockets connection with Circle's real-time messaging system using the `ws` library.

### Basic Setup

Create a WebSocket connection to Circle's server:

```javascript
const WebSocket = require("ws");

const channelName = "ChatRoomChannel"; // Use required channel name

const socket = new WebSocket("wss://app.circle.so/cable", {
  headers: {
    Origin: "https://your-whitelisted-domain.com",
    Authorization: "Bearer HEADLESS_MEMBER_ACCESS_TOKEN"
  }
});
```

{% hint style="info" %}

* The WebSocket URL `wss://app.circle.so/cable` is Circle's specific endpoint.
* Origin should be one of the whitelisted domains added for the community.
* Replace `HEADLESS_MEMBER_ACCESS_TOKEN` with your actual access token.
* Please note that the HEADLESS\_MEMBER\_ACCESS\_TOKEN expires after 1 hour and need to be refreshed.
  {% endhint %}

### Event Handling

#### 1. Connection Open

Handle the connection establishment and channel subscription:

```javascript
socket.on("open", function open() {
  console.log("WebSocket connection opened.");
  
  // Subscribe to the chat channel
  const subscribeMessage = JSON.stringify({
    command: "subscribe",
    identifier: JSON.stringify({
      channel: channelName,
      community_member_id: COMMUNITY_MEMBER_ID
    })
  });
  
  // Send subscription request
  socket.send(subscribeMessage);
  
  // Optional: Send a test message
  socket.send(JSON.stringify({ 
    action: "message", 
    data: "Hello Server!" 
  }));
});
```

{% hint style="info" %}
Make sure to replace the following variables with your actual values:

* COMMUNITY\_MEMBER\_ID: Community member ID who owns the Headless access token.
  {% endhint %}

#### 2. Receiving Messages

Handle incoming messages from the server:

```javascript
socket.on("message", function incoming(data) {
  const message = JSON.parse(data);
  
  if (message.channel === channelName) {
    console.log(`Message from channel ${channelName}:`, message.data);
  } else {
    console.log("Message from server:", message);
  }
});
```

#### 3. Error Handling

Implement error handling for the WebSocket connection:

```javascript
socket.on("error", function error(err) {
  console.error("WebSocket error:", err);
});
```

#### 4. Connection Close

Handle connection closure:

```javascript
socket.on("close", function close() {
  console.log("WebSocket connection closed.");
});
```

### Complete Implementation

Here's the complete code combining all the components:

<pre class="language-javascript"><code class="lang-javascript"><strong>const WebSocket = require("ws");
</strong>const channelName = "ChatRoomChannel";

function initializeWebSocket() {
  const socket = new WebSocket("wss://app.circle.so/cable", {
    headers: {
      Origin: "https://your-whitelisted-domain.com",
      Authorization: "Bearer HEADLESS_MEMBER_ACCESS_TOKEN"
    }
  });

  // Handle connection open
  socket.on("open", function open() {
    console.log("WebSocket connection opened.");
    
    // Subscribe to channel
    const subscribeMessage = JSON.stringify({
      command: "subscribe",
      identifier: JSON.stringify({
        channel: channelName,
        community_member_id: COMMUNITY_MEMBER_ID
      })
    });
    
    socket.send(subscribeMessage);
  });

  // Handle incoming messages
  socket.on("message", function incoming(data) {
    const message = JSON.parse(data);
    if (message.channel === channelName) {
      console.log(`Message from channel ${channelName}:`, message.data);
    } else {
      console.log("Message from server:", message);
    }
  });

  // Handle errors
  socket.on("error", function error(err) {
    console.error("WebSocket error:", err);
  });

  // Handle connection close
  socket.on("close", function close() {
    console.log("WebSocket connection closed.");
    // Optional: Implement reconnection logic here
  });

  return socket;
}

// Initialize the WebSocket connection
const socket = initializeWebSocket();
</code></pre>

### Run Test

```bash
$ node websocket-connection-test.js
```

On successful connection, you will start receiving `ping` from server.&#x20;

Example:

```bash
WebSocket connection opened.
Message from server: { type: 'welcome', sid: 'JnehncGjAQuLBIBoImSMk' }
Message from server: {
  identifier: '{"channel":"ChatRoomChannel","community_member_id": COMMUNITY_MEMBER_ID}',
  type: 'confirm_subscription'
}
Message from server: { type: 'ping', message: 1729855056 }
Message from server: { type: 'ping', message: 1729855059 }
Message from server: { type: 'ping', message: 1729855062 }
```

## Notifications

Name: `*NotificationChannel*`

Pubsub queue: "**notification-channel-#{*****community\_member\_id*****}"**

This channel is used for communication of all real time notifications for a community member. This channel receives following events:

* `newNotification`
  * This event is received whenever community member receives a new in-app notification
* `updateNewNotificationCount`
  * This event is received whenever there is change in community member’s in-app notification count
  * Additional information received with the event:

    ```typescript
    new_notifications_count: number
    ```
* `resetNewNotificationCount`
  * This event is received to mark all notifications as read.
  * Additional information received with the event:

    ```typescript
    new_notifications_count: 0
    ```

## Chat

We have 3 channels to manage websocket communications for messaging.

[👉 chat-room-channel-#{community\_member.id}](#chat-room-channel-community_member.id)

👉 [chat-room-#{chat\_room.id}-channel](#chat-room-chat_room.id-channel)

👉 [chat-community-member-#{community\_member\_id}-threads-channel](#chat-community-member-community_member_id-threads-channel)

#### chat-room-channel-#{community\_member.id}

This channel receives all updates regarding messaging which are specific to a member, all other events from a room which are common for all members are communicated on [chat room channel](https://www.notion.so/80e767bb79a34c09a0d1524c0faf1dea?pvs=21).

#### **Channel details:**

Name: `*ChatRoomChannel*`

Pubsub queue: chat-room-channel-#{community\_member.id}

#### **Events:**

**Chat Room events:**

`chatRoomCreated`

1. This event is broadcasted on a non embedded chat room creation.
2. Additional information received with the event

```typescript
interface ChatRoom {
  id: number;
  uuid: string;
  identifier: string;
  unread_messages_count: number;
  chat_room_kind: string;
  chat_room_name: string;
  chat_room_description: string;
  chat_room_show_history: boolean;
  other_participants_preview: Record<string, any>[];
  current_participant: Record<string, any>;
  last_message: Record<string, any>;
}
```

`chatRoomUpdated`

1. This event gets broadcasted in following cases:
   1. when non embedded chat room name is changed.
   2. when non embedded chat room gets pinned.
2. Please note that same event is broadcasted on `chat-room-#{chat_room.id}-channel` channel on updating room specific attributes like `:pinned_message_id, :show_history, :description, :name` attributes.
3. Additional information received with the event:

   ```typescript
   interface SimplifiedChatRoom {
     uuid: string;
     chat_room_name: string;
     chat_room_description: string;
     chat_room_show_history: boolean;
     pinned_message: Record<string, any>;
   }
   ```

`chatRoomDeleted`

1. This event gets broadcasted on chat room deletion.
2. Additional information received with the event:

   ```typescript
   {
     chat_room_uuid: string
   }
   ```

`chatRoomRead`

1. This event gets broadcasted when chatroom gets marked as read by a participant.
2. Additional information received with the event:

```typescript
{
  chat_room_uuid: string
}
```

`chatRoomPinned`

1. This event gets broadcasted when a non embedded chat room is pinned by a community member.
2. Additional information received with the event:

```typescript
{
  chat_room_id: number
  pinned_at: string
}
```

`chatRoomUnPinned`

1. This event gets broadcasted when a non embedded chat room is un-pinned by a community member.
2. Additional information received with the event:

```typescript
{
  chat_room_id: number
}
```

**ChatRoomMessage events:**

`newMessage`

* This event gets broadcasted for each new non reply message in a non embedded chat room.
* Additional information received with the event:<br>

  ```typescript
  interface JSONMessage {
    id: number;
    chat_room_uuid: string;
    chat_room_kind: string;
    chat_room_participant_id: number; // Assuming this is a number based on previous examples
    body: string;
    rich_text_body: Record<string, any>;
    sent_at: string;
    created_at: string;
    sender: Record<string, any>;
    creation_uuid: string;
    chat_thread_id: number;
    parent_message_id: number;
    lesson_id: number;
    edited_at: string;
    replies_count: number;
    total_thread_participants_count: number;
    thread_participants_preview: Record<string, any>;
    chat_thread_replies_count: number;
  }
  ```
* Note: We are [broadcasting exact same event](https://www.notion.so/Websockets-b0f1bd026bcd41dd833d04f8a036df63?pvs=21) on chat-room-#{chat\_room.id}-channel currently for all new messages including replies. This is sent for all embedded and non embedded rooms.

#### chat-room-#{chat\_room.id}-channel

We are using this channel to post chat room events which are not intended for a specific community member.

#### **Channel details:**

Name: `*Chats*::*RoomChannel*`

Pubsub queue: `chat-room-#{*chat_room*.id}-channel`

#### **Events:**

**ChatRoomMessage events:**

`newMessage`

* This event gets broadcasted for each new message in the chat room
* Additional information received with the event:

```typescript
interface JSONMessage {
  id: number;
  chat_room_uuid: string;
  chat_room_kind: string;
  chat_room_participant_id: number; // Assuming this is a number based on previous examples
  body: string;
  rich_text_body: Record<string, any>;
  sent_at: string;
  created_at: string;
  sender: Record<string, any>;
  creation_uuid: string;
  chat_thread_id: number;
  parent_message_id: number;
  lesson_id: number;
  edited_at: string;
  replies_count: number;
  total_thread_participants_count: number;
  thread_participants_preview: Record<string, any>;
  chat_thread_replies_count: number;
}
```

`deletedMessage`

* This event gets broadcasted when a message gets deleted from the chat room
* Additional information received with the event:<br>

  ```typescript
  interface ParentMessage {
    id: number;
    chat_room_uuid: string;
    chat_room_participant_id: number; // Assuming this is a number based on previous examples
    body: string;
    rich_text_body: Record<string, any>;
    sent_at: string;
    created_at: string;
    sender: Record<string, any>;
    creation_uuid: string;
    chat_thread_id: number;
    parent_message_id: number;
    lesson_id: number;
    edited_at: string;
    replies_count: number;
    total_thread_participants_count: number;
    thread_participants_preview: Record<string, any>;
  }

  interface MessageWithParent {
    id: number;
    parent_message: ParentMessage;
  }
  ```

`updatedMessage`

* This event gets broadcasted when a message is updated
* Additional information received with the event:

```typescript
interface JSONMessage {
  id: number;
  chat_room_uuid: string;
  chat_room_participant_id: number; // Assuming this is a number based on previous examples
  body: string;
  rich_text_body: Record<string, any>;
  sent_at: string;
  created_at: string;
  sender: Record<string, any>;
  creation_uuid: string;
  chat_thread_id: number;
  parent_message_id: number;
  lesson_id: number;
  edited_at: string;
  replies_count: number;
  total_thread_participants_count: number;
  thread_participants_preview: Record<string, any>;
}
```

#### chat-community-member-#{community\_member\_id}-threads-channel

We are using this channel to communicate events for the threads that a community is part of

#### **Channel details:**

Name: `*Chats*::*CommunityMemberThreadsChannel*`

Pubsub queue: `chat-community-member-#{community_member_id}-threads-channel`

`newMessage`

1. This event gets broadcasted for each new message in the chat thread.
2. Additional information received with the event:

```typescript
interface JSONMessage {
  id: number;
  chat_room_uuid: string;
  chat_room_kind: string;
  chat_room_participant_id: number; // Assuming this is a number based on previous examples
  body: string;
  rich_text_body: Record<string, any>;
  sent_at: string;
  created_at: string;
  sender: Record<string, any>;
  creation_uuid: string;
  chat_thread_id: number;
  parent_message_id: number;
  lesson_id: number;
  edited_at: string;
  replies_count: number;
  total_thread_participants_count: number;
  thread_participants_preview: Record<string, any>;
  chat_thread_replies_count: number;
}
```

`updatedMessage`

1. This event gets broadcasted when a message is updated in chat thread
2. Additional information received with the event:

```typescript
interface ChatMessage {
  id: number;
  chat_room_uuid: string;
  chat_room_participant_id: number; // Assuming this is a number based on the comment
  body: string;
  rich_text_body: Record<string, any>; // Using Record for a generic object type
  sent_at: string;
  created_at: string;
  sender: Record<string, any>;
  creation_uuid: string;
  chat_thread_id: number;
  parent_message_id: number;
  lesson_id: number;
  edited_at: string;
  replies_count: number;
  total_thread_participants_count: number;
  thread_participants_preview: Record<string, any>;
}
```

`deletedMessage`

1. This event gets broadcasted when a message gets deleted from the chat thread
2. Additional information received with the event:

```typescript
interface ParentMessage {
  id: number;
  chat_room_participant_id: string;
  rich_text_body: Record<string, any>;
  created_at: string;
  sender: Record<string, any>;
  creation_uuid: string;
  chat_thread_id: number;
  parent_message_id: number;
  lesson_id: number;
  edited_at: string;
  replies_count: number;
  total_thread_participants_count: number;
  thread_participants_preview: Record<string, any>;
}

interface MessageWithParent {
  id: number;
  parent_message: ParentMessage;
}
```

`chatThreadRead`

1. This event gets broadcasted when chatroom gets marked as read by a participant.
2. Additional information received with the event:

```typescript
{
  chat_thread_id: number
}
```


# Security and privacy


# Circle MCP

Connect AI tools like Claude, Cursor, and ChatGPT directly to your Circle community. Manage members, create posts, check analytics, and more — just by asking

### Set up

The connection URL you'll need is:

```
https://app.circle.so/api/mcp
```

When you connect for the first time, you'll sign in with your Circle account and choose what the AI can do — just read your data, or read and make changes.

#### Claude ([claude.ai](http://claude.ai), Cowork, and Claude Desktop)

**Pro and Max plans**

1. Go to **Customize → Connectors**
2. Click **"+"** then **"Add custom connector"**
3. Enter the URL above
4. Click **"Add"**, then follow the sign-in prompts

**Team and Enterprise plans**

1. An Owner must first add the connector: **Organization settings → Connectors → Add → Custom → Web**
2. Enter the URL above and click **"Add"**
3. Members can then find it under **Customize → Connectors** and click **"Connect"**

**Enabling in chat:** Click the **"+"** button in the lower left of your chat, then **"Connectors"** to toggle Circle on or off per conversation.

#### Claude Code (terminal)

```bash
claude mcp add --transport http circle https://app.circle.so/api/mcp
```

Then run `/mcp` in your session and follow the sign-in prompt.

#### Cursor

1. Open **Settings → Tools and MCP → Add new global MCP server**
2. Paste:

```json
{
  "mcpServers": {
    "circle": {
      "url": "https://app.circle.so/api/mcp"
    }
  }
}
```

1. Save, restart Cursor, and sign in when prompted

#### ChatGPT

Custom MCP apps in ChatGPT are in beta and require **Developer mode** to be enabled.

**Step 1: Enable Developer mode**

1. Go to **Settings → Apps → Advanced Settings**
2. Turn on **Developer mode**

**Step 2: Create the Circle app**

1. Go to **Settings → Apps → Create** (or **Workspace Settings → Apps → Create** if you're an admin)
2. Enter:
   * **Name:** Circle
   * **MCP Server URL:** `https://app.circle.so/api/mcp`
   * **Authentication:** OAuth
3. Click **Create**

**Step 3: Connect**

* The app will appear under **Settings → Apps → Enabled Apps** with a **Dev** label
* Click **Connect** and follow the sign-in prompts

**For workspace admins:**

* The app starts as a **Draft** in **Workspace Settings → Apps**
* Review it, then click **Publish** to make it available to your workspace
* You can also configure which actions the app is allowed to take before publishing

#### VS Code (GitHub Copilot)

1. Open the Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) and run **MCP: Add Server**
2. Choose **HTTP** as the type
3. Enter the URL above and name it **Circle**
4. Start the server and complete the sign-in

Or add it manually to `.vscode/mcp.json`:

```json
{
  "servers": {
    "circle": {
      "type": "http",
      "url": "https://app.circle.so/api/mcp"
    }
  }
}
```

#### Windsurf

1. Open **Settings** (`Cmd+,`) → search **MCP** → click **View raw config**
2. Add:

```json
{
  "mcpServers": {
    "circle": {
      "serverUrl": "https://app.circle.so/api/mcp"
    }
  }
}
```

1. Save and restart

#### Other AI tools

Most MCP-compatible tools will ask for either a URL or a JSON config. Use:

```
https://app.circle.so/api/mcp
```

If your tool doesn't support remote connections directly, use this as a workaround:

```json
{
  "mcpServers": {
    "circle": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://app.circle.so/api/mcp"]
    }
  }
}
```

***

### Access levels

You pick one when you connect. You can always change it later by disconnecting and reconnecting.

**Read only** — The AI can view your community: members, spaces, posts, events, analytics, and more. It can't create, change, or delete anything. Write tools are completely hidden.

**Full access** — The AI can also take action: invite members, create posts, manage events, send messages, and more.

> We recommend starting with **Read only** and switching to Full access only when you need it.

***

### What you can do

Here's what the AI can help with, depending on your access level:

**View and explore** (both levels)

* Browse members, spaces, posts, events, courses, and tags
* Search for specific members or content
* View analytics, form submissions, and live room transcripts
* Get AI-powered summaries of space activity

**Create and manage** (Full access only)

* Invite, update, or remove members
* Create, edit, or delete posts, comments, and events
* Set up courses, sections, and lessons
* Send direct messages and chat messages
* Manage tags, segments, and access groups
* Upload files and images

***

### Billing and usage

Circle MCP runs on the Admin API v2, so every action the AI takes counts as one API request against your community's monthly limit.

| Plan                     | Monthly API requests |
| ------------------------ | -------------------- |
| Business                 | 5,000                |
| Enterprise / Circle Plus | 30,000               |
| Circle Plus Platform     | 250,000              |

You can see your current usage in **Settings → Developers** in your community.

A few things to keep in mind:

* Each tool call = one API request (for example, "show me all members" is one request, "create a post" is one request)
* There's also a rate limit of 2,000 requests per 5 minutes
* If you hit your monthly limit, the AI will let you know it can't complete the action

See [Usage and limits](https://api.circle.so/apis/admin-api/usage-and-limits) for full details.

***

### FAQ

**Can multiple admins connect?**

Yes. Each admin connects with their own account and picks their own access level.

**How do I change my access level?**

Disconnect and reconnect with the new level. Your old connection is automatically revoked.

**The AI says it can't perform an action. What do I do?**

If you're connected with Read only, write actions won't be available. Disconnect and reconnect with Full access. If you've hit your API usage limit, check **Settings → Developers** in your community.

***

### Feedback

* [Developer community](https://community.circle.so/c/developers/)
* [Send us feedback](https://circleco.typeform.com/to/xFEpyITZ)


# Prompt library

Your community is now connected to AI. Here are ready-to-use prompts to help you manage, understand, and grow your Circle community — organized by what you're trying to accomplish.

{% hint style="info" %}
How to use this: Copy any prompt into your preferred MCP-compatible LLM with [Circle MCP](https://circle.so/mcp) connected. Swap the \[bracketed placeholders] with your own details. Start with Get Oriented if this is your first time.

Learn more: [what is Circle MCP](https://circle.so/mcp) and [MCP documentation](https://api.circle.so/mcp)
{% endhint %}

### Jump to a section

These sections start with ***reading*** — understanding your community’s data, spotting patterns, and surfacing insights. As you go, the prompts shift into ***doing***: creating content, managing members, configuring spaces, and running cross-tool workflows. Start with Get Oriented and progress down the list to become familiar with Circle MCP’s capabilities.

* [**Get Oriented**](#get-oriented) — First time? Start here
* [**Community Health Reports**](#community-health-reports) — Weekly reports, daily pulse checks, engagement trends
* [**Member Engagement & Re-Engagement**](#member-engagement-and-re-engagement) — Find silent members, prevent churn, draft outreach
* [**Content Mining & Repurposing**](#content-mining-and-repurposing) — Turn community data into courses, newsletters, and copy
* [**Content Creation & Management**](#content-creation-and-management) — Create posts, find what's resonating, respond to members
* [**Strategy & Growth**](#strategy-and-growth) — What to focus on, what to stop, what to build next
* [**Triage & Operations**](#triage-and-operations) — Support triage, moderation, sentiment monitoring
* [**Events**](#events) — Create, manage, and follow up on events
* [**Cross-Tool Workflows**](#cross-tool-workflows) — Combine Circle with Slack, Gmail, CRM, Granola, and more
* [**Spaces, Structure & Settings**](#spaces-structure-and-settings) — Create spaces, configure settings, manage SEO
* [**Member Management & Access Groups**](#member-management-and-access-groups) — Invite, tag, look up, and manage access
* [**Tips**](#tips-for-getting-the-most-out-of-circle-mcp) — Get better results from your prompts
* [**Making It Repeatable**](#making-it-repeatable) — Build operational rhythms and automation

#### 💡 Tips for Getting the Most Out of Circle MCP

**Start by reading, then start writing.** Begin with prompts that pull data (list, search, show). Once you're comfortable, move to actions (create, update, invite). Circle MCP lets you require approval before any write action goes through.

**Be specific about where.** "Show me posts" is vague. "Show me the latest 10 posts in the Getting Started space sorted by likes" gets you exactly what you need.

**Chain actions together.** You can do multiple things in one prompt: "Create a space, add three posts to it, and invite five members." MCP handles multi-step workflows naturally.

**Build on what works.** Once you find a prompt that gives you value, save it. Many community managers run the same handful of prompts daily or weekly — community reports, moderation triage, unanswered post scans. These become your operational rhythms.

**Combine with other tools.** Circle MCP works alongside any other connector in the same session. Pull from your CRM, check your calendar, post to Slack — all in one conversation.

**Iterate.** If a result isn't right, refine your prompt. Ask Claude to adjust, filter differently, or reformat. This is a conversation, not a form.

## 🧭 Get Oriented

First time connecting? Start here. These prompts help you understand what you're working with before you take any action.

#### Get a community snapshot `🔍 read`

```
Give me a full overview of my Circle community — name, total members, all spaces organized by space group, and any notable settings. I want to understand the lay of the land.
```

#### Audit my space structure `🔍 read`

```
List every space in my community grouped by space group. For each one, tell me the type (basic, event, course, chat, image), whether it's private or public, hidden from non-members, and what display view it uses.
```

#### Review my access groups `🔍 read`

```
List all my active access groups with their names, descriptions, and member counts. Are any of them empty or barely used?
```

#### Check my member tags `🔍 read`

```
Show me all my member tags — names, colors, whether they're public, and where they display (directory, post bio, profile). Are there any that overlap or should be consolidated?
```

## 📊 Community Health Reports

Turn your community data into actionable insights — the prompts customers reach for first and run most often.

#### Generate a weekly community report `🔍 read`

```
Pull the last 7 days of activity across my community. For each space, give me: total new posts, total comments, and the single most-engaged post. Then give me an overall summary — what's trending, what's quiet, what needs attention.
```

#### Get a daily pulse check `🔍 read`

```
Summarize today's activity across my community. Which spaces had new posts? Were there any flagged items? Any posts with zero responses that I should jump on?
```

#### Build a monthly engagement report `🔍 read`

```
Analyze the last 30 days of my community. Give me: total new members, most active spaces by post count, most-liked posts, spaces with declining activity, and any patterns worth noting. Format it as a report I can share with my team.
```

#### Audit my moderation queue `🔍 read`

```
Show me all flagged content still in the inbox. For each item, tell me the content type (post, comment, message), the reason it was flagged, and who reported it. Prioritize by severity.
```

***

## 👥 Member Engagement & Re-Engagement

Find the members who need attention — the ones who showed up but haven't engaged, your power users, and everyone in between.

#### Find silent members who never activated `🔍 read`

```
Look at members who joined in the last 30 days. Which ones have never posted or commented in any space? Give me their names and emails — these are my activation priorities.
```

#### Identify members going quiet `🔍 read`

```
Which members were active in the last 60 days but have had zero activity in the last 14 days? These might be disengaging — I want to reach out before they churn.
```

#### Draft personalized re-engagement outreach `🔍 read`

```
I have [X] members who joined recently but haven't posted or commented yet. For each one, draft a short, warm direct message inviting them to introduce themselves. Vary the tone — make it feel personal, not templated.
```

#### Find your most engaged members `🔍 read`

```
Search for the most-liked posts across my community. Who are the authors showing up repeatedly? Give me a list of my top contributors — I want to recognize them.
```

#### Build a community leaderboard `🔍 read`

```
Create a leaderboard of my most active members this month based on posts, comments, and likes received. Rank them and include their names, activity counts, and which spaces they're most active in. I want to use this to recognize top contributors and understand who's driving conversations.
```

#### Spot members at risk in a paid tier `🔍 read`

```
List all members in the "[Access Group Name]" access group. Cross-reference with their recent activity — have any of them gone silent? These are paying members I might lose.
```

#### Find members ready for your next tier `🔍 read`

```
Look at members who are NOT in the "[Premium Access Group]" access group but are highly active — posting, commenting, and engaging regularly. These are people who might be a great fit for an upgrade. Give me a list with their names, emails, and a summary of their activity.
```

#### Segment members by behavior `🔍 read`

```
Group my members into segments based on their activity: highly active (posting and commenting regularly), moderately active (occasional engagement), lurkers (joined but rarely participate), and inactive (no activity in 30+ days). How many are in each group? This is my baseline for understanding community health.
```

#### Welcome a new member personally `🔍 read`

```
Look up the member at [email] in my community. Check their profile, headline, spaces, tags, and access groups. Based on who they are and what they have access to, draft a personalized welcome message I can send them as a DM in Circle — or post as a comment on their intro post if they've already introduced themselves.
```

***

## 🔍 Content Mining & Repurposing

Use your community's existing content as raw material for courses, books, campaigns, and more.

#### Surface what members are asking for `🔍 read`

```
Search across my community for posts and comments where members ask questions, request help, or describe problems they're trying to solve. Cluster these into themes and rank them by frequency. I want to know what my members need most — this could inform new content, courses, or offerings.
```

#### Extract the language your members actually use `🔍 read`

```
Search posts and comments in [space name] where members describe their challenges, goals, or what they're struggling with. Pull out the exact words and phrases they use — not summarized, but their actual language. I want to use this for writing copy, course descriptions, and messaging that sounds like my audience, not like me.
```

#### Build a story library from member posts `🔍 read`

```
Search across my community for posts where members share wins, results, transformations, or testimonials. Pull out the key details — who they are, what they accomplished, and any specific numbers or outcomes. Compile this into a list of real customer stories I can reference.
```

#### Create a content campaign from engagement data `🔍 read`

```
Analyze the most popular posts and comments from the last 90 days. Based on what gets the most engagement, suggest a 30-day content calendar for [space name] with post titles and brief descriptions.
```

#### Turn a popular thread into a lesson or workshop `🔍 read`

```
Find the most-engaged discussion thread in [space name] — the one with the most comments and likes. Break down what was discussed, the key questions members asked, and the insights shared. Then outline how I could turn this into a structured lesson, workshop, or live session.
```

#### Extract a FAQ from community questions `🔍 read`

```
Search for posts and comments that are formatted as questions across [space name]. Compile the best Q&A pairs into a structured FAQ document I can publish or turn into a lesson.
```

#### Write a newsletter from community highlights `🔍 read`

```
Summarize the best posts, discussions, and member wins from the last 7 days across my community. Format it as a weekly newsletter I can send to my audience — highlighting what's happening inside without giving away everything.
```

#### Package community content into a lead magnet `🔍 read`

```
Analyze the most valuable content in my community — the posts with the most engagement, the questions that come up most often, and the resources members reference repeatedly. Based on what you find, suggest how I could package this into a free resource (a guide, checklist, toolkit, or mini-course) that I can use to attract new members. Outline what it would cover, a working title, and which community content to pull from.
```

***

## ✏️ Content Creation & Management

Create posts, find what's resonating, and keep discussions moving.

#### Create an announcement `✏️ write`

```
Create a published post in [space name] titled "[Title]" with this body: [content]. Pin it to the top.
```

#### Find your best-performing content `🔍 read`

```
Show me the most-liked posts in [space name] from the last 30 days. I want to see what topics and formats resonate so I can create more of it.
```

#### Find unanswered posts `🔍 read`

```
Look at the latest 20 posts in [space name]. Which ones have zero comments? These are conversations that need a response — list them with titles and authors.
```

#### Respond to unanswered posts `✏️ write`

```
Find posts in [space name] with zero comments. For each one, draft a helpful reply based on what the member is asking about. Show me the drafts before posting.
```

#### Search for posts on a topic `🔍 read`

```
Search across my community for posts mentioning "[keyword]." Show me the title, author, space, and number of comments for each result.
```

***

## 🚀 Strategy & Growth

Higher-altitude prompts for using your community data to make better decisions — about what to build, where to focus, and what's working.

#### What should I focus on this week? `🔍 read`

```
Look at everything happening in my community right now — engagement trends, unanswered posts, flagged content, quiet spaces, new members. Based on what you see, what are the 3 highest-impact things I should focus on this week?
```

#### What's working and what isn't? `🔍 read`

```
Compare engagement across all my spaces over the last 30 days. Which spaces are growing? Which are declining? Are there posts or topics that consistently outperform? Give me an honest assessment of what's working and where I'm losing momentum.
```

#### Find the biggest bottleneck `🔍 read`

```
Based on everything in my community — member activity, engagement patterns, content performance, quiet spaces, unanswered posts — what is the single biggest thing holding my community back right now? Don't give me a dashboard. Give me one clear bottleneck and what I should do about it.
```

#### What should I stop doing? `🔍 read`

```
Look at my spaces, content, and community activity. Are there spaces with consistently low engagement that might not be worth maintaining? Content types that get no traction? Tell me what I should consider sunsetting or consolidating so I can focus my energy where it matters.
```

#### Suggest experiments to try `🔍 read`

```
Based on the current state of my community — activity levels, member behavior, content performance — suggest 3 small experiments I could run this week to improve engagement or retention. Keep them low-effort and measurable.
```

#### Identify what members would pay for `🔍 read`

```
Analyze the most common questions, problems, and requests members are posting about in [space name]. Which of these feel like the kind of thing people would pay to get help with? Group them by theme and give me your read on which ones have the most energy behind them.
```

#### Design a better onboarding experience `🔍 read`

```
Look at the members who joined in the last 90 days. What did the most active ones do in their first week — which spaces did they join, what did they post, what did they engage with? Compare that to the members who went silent. What's different? Use this to recommend an onboarding sequence.
```

#### Document my workflows into repeatable SOPs `🔍 read`

```
Based on the prompts and workflows I've been running with Circle MCP — community reports, moderation triage, member outreach — help me turn these into documented standard operating procedures. For each one, outline: what it does, when to run it, and the exact prompt to use. I want my team (or a future hire) to be able to run these without me.
```

#### Plan what I should automate `🔍 read`

```
Look at my community's activity patterns — what happens repeatedly? New members joining, posts going unanswered, events needing follow-ups, content going stale. Based on what you see, recommend which tasks I should automate using Circle's built-in Workflows (trigger → action automations) and which ones are better handled with a recurring MCP prompt. I want to spend less time on repetitive work.
```

***

## 🛡️ Triage & Operations

For community managers running daily operations — support triage, moderation, and issue ownership.

#### Triage support requests `🔍 read`

```
Pull the latest posts from [support space]. Categorize each one: billing, technical issue, feature request, or general question. Flag any with zero responses — those need immediate attention.
```

#### Daily moderation triage `🔍 read`

```
Show me all flagged content in the inbox. For each item, give me: content type, reason flagged, who reported it, and a recommended action (approve, reject, or escalate). I want a clear queue to work through.
```

#### Flag problematic content `✏️ write`

```
Flag post [ID] for moderation. Reason: [harassment / spam / against guidelines / other]. Note: "[description]."
```

#### Spot negative sentiment `🔍 read`

```
Search for comments in [space name] containing words like "frustrated," "disappointed," "cancel," "broken," or "not working." Show me the post, the comment, and the author. I want to triage these personally.
```

***

## 📅 Events

Create, manage, and follow up on community events.

#### Create a live event `✏️ write`

```
Create a published event in [events space] called "[Name]" on [date] at [time]. Make it a live stream with speaker view, recording enabled, open to all members, and 60 minutes long.
```

#### Set up a recurring weekly session `✏️ write`

```
Create a recurring weekly event in [events space] called "[Name]" starting [date] at [time]. Run for 12 weeks. Use a live room with grid view.
```

#### Find upcoming events `🔍 read`

```
Search for all upcoming events in my community. List the name, date, space, and format (virtual, live stream, in-person).
```

#### Prep for an upcoming event `🔍 read`

```
I have an upcoming event called "[Event Name]" in my community. Look up who's RSVP'd, check their member profiles, tags, access groups, and recent activity. Give me a briefing: who's attending, how active they are in the community, and anything notable about them. I want to go into this event knowing who's in the room so I can personalize the experience and follow up with the right people afterward.
```

#### Follow up after an event `🔍 read`

```
Look up the event "[Event Name]" in my community and check who attended. Draft a follow-up post for [events space] thanking attendees, recapping the top 3 takeaways from the session, and inviting anyone who missed it to check out the recording.
```

***

## 🔗 Cross-Tool Workflows

Combine Circle MCP with your other connected tools for workflows that span multiple platforms.

#### CRM → community onboarding `✏️ write` `[HubSpot / Salesforce / your CRM]`

```
Check my CRM for recently closed deals. For each new customer, invite them to my Circle community, add them to the "[Customers]" access group, and tag them "[New Customer]."
```

#### Community signals → Slack `🔍 read` `[Slack]`

```
Search my Circle community for flagged content and unanswered posts from the last 24 hours. Summarize the key issues and draft a message for the [#community-ops] Slack channel.
```

#### Community data → team report `🔍 read` `[Google Drive / Slack / Gmail]`

```
Pull the last 30 days of community data — new members, top posts, engagement by space — and create a formatted report I can share with my team via [Google Drive / Slack / email].
```

#### Event follow-up via email `🔍 read` `[Email tool]`

```
Look up the attendees for "[Event Name]" in my Circle community. For each attendee, draft a personalized follow-up email thanking them for attending and sharing a link to the recording or recap post. Send via [Gmail / your email tool].
```

#### Calendar → event creation `✏️ write` `[Google Calendar]`

```
Check my Google Calendar for upcoming workshops this month. For each one, create a matching event in my [events space] with the correct date, time, and Zoom link from the calendar invite.
```

#### Live Room recap → post + email `🔍 read` `[Circle + Gmail / Email tool]`

```
Pull the transcript from my Circle Live Room for "[Event or Session Name]." Summarize the key takeaways, any action items or next steps, and the most common questions that came up during the session. Then: (1) draft a recap post for [space name] in my Circle community that includes the summary, key takeaways, and a Q&A section from the questions asked, and (2) draft a follow-up email via [Gmail] to attendees with the same recap and a link back to the community post.
```

#### Call recording recap → post + email `🔍 read` `[Granola / Fathom / Notion + Circle + Gmail]`

```
Pull the transcript or notes from my recent session "[Session Name]" in [Granola / Fathom / Notion]. Summarize the key takeaways, any action items or commitments, and the most interesting questions from attendees. Then: (1) draft a recap post for [space name] in my Circle community with the summary and a Q&A section built from the questions asked during the call, and (2) draft a follow-up email via [Gmail] sharing the recap and linking to the community post. If there were enough questions to warrant it, also draft a standalone FAQ or Q&A post I can pin in [space name] as a resource for members.
```

***

## 🏗️ Spaces, Structure & Settings

Create, configure, and reorganize your community's spaces, groups, and settings.

#### Create a discussion space `✏️ write`

```
Create a new basic space called "[Name]" in the [Space Group] space group. Set it to the feed display view, make it public, and use the emoji [emoji].
```

#### Create a course `✏️ write`

```
Create a new course space called "[Name]" in the [Space Group] space group. Make it structured with enforced lesson order. Use "Module" for sections and "Lesson" for lessons.
```

#### Add course content `✏️ write`

```
In my [Course Name] course, create a section called "[Section Name]." Then add these lessons (all as drafts): "[Lesson 1]", "[Lesson 2]", "[Lesson 3]."
```

#### Create a chat room `✏️ write`

```
Create a chat space called "[Name]" in the [Space Group] space group. Show chat history to new members.
```

#### Create a new space group `✏️ write`

```
Create a new space group called "[Name]." Automatically add members to new spaces in this group. Hide it from non-members.
```

#### Set up a gated/locked space `✏️ write`

```
Make [space name] private and hidden from non-members. Set the lock screen heading to "[Heading]," description to "[Description]," and CTA button to "[Label]" linking to [URL].
```

#### Update weekly digest copy `✏️ write`

```
Change my weekly digest subject line to "[Subject]" and intro text to "[Intro]."
```

#### Set custom locked-post CTA `✏️ write`

```
Set the locked post CTA: heading "[Heading]," body "[Body]," button "[Label]" linking to [URL].
```

#### Update space SEO `✏️ write`

```
Update the meta title for [space name] to "[Title]" and meta description to "[Description]." Match the Open Graph tags.
```

#### Configure notification defaults for a space `✏️ write`

```
Update [space name] so that new members get email notifications for new posts, in-app notifications for mentions, and mobile push notifications for mentions. I want to make sure members don't miss important activity without being overwhelmed.
```

***

## 👤 Member Management & Access Groups

Invite, organize, look up, and manage your members and their access.

#### Invite a new member `✏️ write`

```
Invite [name] at [email]. Add them to the [Space Group] space group and tag them with "[Tag]."
```

#### Bulk invite `✏️ write`

```
Invite these people and add them to "[Space Group]": [email1], [email2], [email3]. Skip invitation emails for now.
```

#### Look up a member `🔍 read`

```
Look up [email]. Show me their name, headline, tags, spaces, and access groups.
```

#### Tag members by cohort `✏️ write`

```
Create a member tag called "[Tag]" in [color], visible on profiles and in the directory. Then apply it to: [email1], [email2], [email3].
```

#### Deactivate a member `✏️ write`

```
Deactivate the member at [email].
```

#### Create an access group `✏️ write`

```
Create an access group called "[Name]" with description: "[Description]."
```

#### Add members to an access group `✏️ write`

```
Add these members to "[Access Group]": [email1], [email2], [email3].
```

#### Audit who's in an access group `🔍 read`

```
List all members in "[Access Group]" — names and emails. How many total?
```

#### Check a specific member's access `🔍 read`

```
What access groups does [email] belong to? What spaces do those groups unlock?
```

#### Remove a member from an access group `✏️ write`

```
Remove [email] from "[Access Group]."
```

***

## 🔁 Making It Repeatable

Circle MCP runs inside a conversation — it doesn't schedule prompts on its own. But the most valuable prompts in this library are ones you'll want to run on a rhythm: your weekly community report, your moderation triage, your unanswered post scan. Here's how to make that easy.

**Save your go-to prompts in a Claude Project.** Create a Claude Project for your community operations and save your most-used prompts there (along with any context about your community, like space names or access group names). When it's time to run your weekly report, open the project and go — no retyping, no remembering.

**Use Circle's built-in Workflows for event-driven automation.** Some things you'd want to automate — like welcoming new members, notifying your team when someone posts in a specific space, or tagging members based on actions — can be handled natively in Circle using Workflows (trigger → action). No MCP needed. Check your community's Settings → Workflows to see what's available.

**Connect Zapier for scheduled triggers.** If you want a prompt to truly run on a schedule — like "every Monday, pull my community report and send it to Slack" — you can use Zapier (which supports MCP) to trigger workflows on a timer. This takes more setup, but it's the closest thing to set-it-and-forget-it automation with MCP today.

**For technical users: Claude Code + a scheduler.** If you're comfortable with the command line, Claude Code supports MCP connections. You can write a script that connects to your Circle community, runs a specific prompt, and outputs the result to Slack, email, or a file — then schedule it with a cron job or task scheduler. This is the most flexible option for fully automated workflows.


# Admin API

The Circle Admin API is designed for community admins to build automations, migration scripts, and administrative integrations.

<figure><img src="/files/QhNfHdXVyo8iMERBWV6i" alt=""><figcaption></figcaption></figure>

> Note: the admin API is not meant to be used on the client side, or to re-create your own Circle experience from scratch. If you're looking to build client-side features for your members to interact with your community in your own website or app, please refer to our [Headless API](/apis/headless).

### Fetch your API token

Community admins can obtain an API token by going to the **Developers -> Tokens** page in their community. Please keep your key private — do not share your key with anyone or make it publicly accessible in any form.

### API versions

In September 2024, we'll be introducing the Admin API v2 which addresses several limitations with the v1 API, including:

1. Lack of versioning
2. OpenAPI spec compatibility
3. Performance issues with large datasets
4. Missing endpoints

### See endpoints

* [V1](https://api-v1.circle.so/)
* [V2](https://api-headless.circle.so/?urls.primaryName=Admin%20APIs#/) (default)

#### Which API version should I use?

We **strongly recommend** using the admin API v2 whenever possible, and updating your codebase to v2 endpoints if you've built automations with the v1 API.

While we don't plan to deprecate the v1 API any time soon, new endpoints and updates will only be added to our v2 API going forward.&#x20;

### Paginated requests

1. **Pagination params**:
   * `page`: This parameter allows you to specify which page of results you want to retrieve. If not provided, it defaults to page 1.
   * `per_page`: This parameter lets you set how many items you want per page. If not specified, it will default to 10 items per page.
2. **Response structure**:
   * `page`: The current page number
   * `per_page`: The number of items per page
   * `has_next_page`: A boolean indicating if there are more pages after the current one
   * `count`: The total number of items across all pages
   * `page_count`: The total number of pages
   * `records`: An array containing the data for the current page
3. **Example usage**: To get the second page with 30 items per page, you would make a request like this:

```bash
curl -X GET "https://app.circle.so/api/headless/admin/v1/posts?page=2&per_page=30" \
     -H "Authorization: Bearer <API_Token>" \
     -H "Content-Type: application/json"
```

Remember, when implementing pagination in your application, it's good practice to respect the `has_next_page` flag and try not request pages beyond what's available. Also, be aware that the total count might change between requests if new posts are added or removed.

### Feedback

* If you have general questions or want to share your creations with the developer community, please check out our [Developer community space](https://community.circle.so/c/developers/).&#x20;
* If you have API feedback for our engineering team, [please use this form](https://circleco.typeform.com/to/xFEpyITZ#email=xxxxx\&visitor=xxxxx) to reach out to us.


# Quick start

To get started with the Admin API, follow these steps.

### Authentication

The admin API uses **token-based authentication**.

Your unique API token identifies your community within Circle's server and enables you to perform administrative tasks, such as handling community data or generating new content.

Community admins can obtain an API key by going to the **Developers -> Tokens** page in their community and selecting the type as **Admin V1 or V2**. Also, we recommend reading through the [API Docs here](https://api-headless.circle.so/?urls.primaryName=Admin%20APIs#/).

#### Request headers

After getting your API token, you can pass it as a header parameter with every request you make to the admin API:

```json
{
    "Authorization": "Bearer <API_Token>",
    "Content-Type": "application/json"
}
```

### Performing a request

As an example, if you'd like to retrieve all posts for a specific space, you can use the following endpoint with the space's unique identifier. In this instance, posts are being fetched within the space ID `999`:

```sh
curl -X GET "https://app.circle.so/api/admin/v2/comments/posts?space_id=999&page=1&per_page=20&status=published" \
     -H "Authorization: Bearer <API_Token>" \
     -H "Content-Type: application/json"
```

This request also contains parameters like `page`, `per_page` and `status:`

```json
// simplified version of Posts just 
{
  "page": 1,
  "per_page": 20,
  "has_next_page": false,
  "count": 3,
  "page_count": 1,
  "records": [
    {
      "id": 1001,
      "status": "published",
      "name": "Welcome to Our Product Space",
      "slug": "welcome-to-our-product-space",
      "body": {
        "id": 1001,
        "body": "<p>Welcome to the product discussion space! Here we'll share updates and gather feedback.</p>",
        "created_at": "2024-09-01T09:00:00.000Z",
        "updated_at": "2024-09-01T09:00:00.000Z"
      },
      "url": "https://app.circle.so/c/product-space/welcome-to-our-product-space",
      "space_name": "Product Space",
      "space_id": 999,
      "user_name": "Product Manager",
      "comments_count": 15,
      "published_at": "2024-09-01T09:00:00.000Z",
      "likes_count": 32
    },
    {
      "id": 1002,
      "status": "published",
      "name": "New Feature Announcement",
      "slug": "new-feature-announcement",
      "body": {
        "id": 1002,
        "body": "<p>We're excited to announce our latest feature: AI-powered recommendations!</p>",
        "created_at": "2024-09-05T14:30:00.000Z",
        "updated_at": "2024-09-05T14:30:00.000Z"
      },
      "url": "https://app.circle.so/c/product-space/new-feature-announcement",
      "space_name": "Product Space",
      "space_id": 999,
      "user_name": "Development Lead",
      "comments_count": 28,
      "published_at": "2024-09-05T14:30:00.000Z",
      "likes_count": 45
    },
    {
      "id": 1003,
      "status": "published",
      "name": "Upcoming Maintenance Schedule",
      "slug": "upcoming-maintenance-schedule",
      "body": {
        "id": 1003,
        "body": "<p>Please be aware of our upcoming maintenance window this Saturday from 2 AM to 4 AM EST.</p>",
        "created_at": "2024-09-08T11:00:00.000Z",
        "updated_at": "2024-09-08T11:00:00.000Z"
      },
      "url": "https://app.circle.so/c/product-space/upcoming-maintenance-schedule",
      "space_name": "Product Space",
      "space_id": 999,
      "user_name": "Support Team",
      "comments_count": 7,
      "published_at": "2024-09-08T11:00:00.000Z",
      "likes_count": 12
    }
  ]
}
```


# Usage and limits

### API Usage

Each eligible plan has an allotted number of Admin API requests, with higher-tier plans offering increased request limits. This gives you more flexibility as your needs grow.

#### Plans

* Business - 5,000 requests/month
* Enterprise and Circle Plus - 30,000 requests/month
* Circle Plus Platform - 250,000 requests/month

**Note that Zapier usage does not count towards these limits.**

You can monitor your usage by going to the **Developers** tab in your admin area.

If you exceed your limits over the next few months, don’t worry: we won’t be enforcing our API limits until **January 1, 2025** so you have time to establish a baseline and upgrade your plan if needed.&#x20;

#### Rate limits

To prevent attacks and/or abuses, there's a rate limiting mechanism in place on all of Circle's APIs. The limit is **2000 request per 5 minutes per IP.  That number can change at any time.**\
\
It's important to note that this limit is quite generous compared to other API providers. As a best practice, when you receive a 429 (Too Many Requests) response, implement a backoff strategy by pausing your script and retrying later, ensuring that your requests are within the rate limit.

### FAQ

<details>

<summary>Why isn't my API usage count updated immediately after making API requests?</summary>

API usage counts are not updated in real-time due to backend processing. We cache the usage count for one minute on our side, so you can expect the count to be updated \~5min after performing that call.

</details>

<details>

<summary>Which response codes are considered as part of my limit?</summary>

We consider API requests with the following response codes for updating the API usage count:

200: OK \
201: Created\
204: No Content\
400: Bad Request\
401: Unauthorized\
403: Forbidden\
404: Not Found\
405: Method Not Allowed\
422: Unprocessable Entity\
429: Too Many Requests

Any 500 or other internal server errors are not computed and will not count as a request.

</details>


# Optimizing usage

To avoid exceeding your plan limits, we recommend optimizing your API requests to ensure you aren't being billed for redundant, error-prone, or bad requests.

### Bad requests

While 50X server error codes are excluded from the requests count, [we include 4XX](/apis/admin-api/usage-and-limits) error codes since these indicate issues with the request itself.

#### Empty params

A common issue for 400, 422 and 404 errors is a blank or missing param.&#x20;

```bash
// good request
GET /api/admin/v2/community_members/123

// bad request
GET /api/admin/v2/community_members/undefined
GET /api/admin/v2/community_members
```

#### Bad token

401 and 403 are also taken into account as part of your limit quota, so ensure requests include valid, non-expired authentication tokens.

Tokens are type-specific as well — for example, Admin V2 tokens won't work on Headless Auth API. A wrong token type will also result in a 403.

#### Method not allowed

Double check your HTTP methods (GET, POST, PUT, DELETE, PATCH) to ensure you're sending through the ones allowed on each endpoint. Methods that are not allowed will result in a 405.

#### Rate limit

Sending a lot of requests in a small timeframe can lead to a 429 (Too Many Requests). Our [rate limit policy](#rate-limit) is generous, but when you encounter a response code 429, we recommend waiting for around 60 seconds to send through more requests.

It might helpful for you to implement waits and delays in your code to avoid exceeding rate limits.

### Optimizing endpoints

The "Endpoints overview" section in your Developer dashboard shows you an overview of your requests broken up by endpoint.

<figure><img src="/files/Hgmjb3P4UgNXxworRJE2" alt=""><figcaption></figcaption></figure>

#### Single x Multiple calls

When an API endpoint allows multiple params for bulk actions, we recommend using these instead of making multiple requests.\
\
For example, to add a member to multiple spaces or space groups, you can pass a list of space\_ids or space\_group\_ids in a single call instead of making multiple requests to add members to each space.

#### Caching

A caching layer on your end can help you make requests only when you need to.&#x20;

For example, we recommend caching static datasets which don't change that often; i.e. spaces or community members.

#### Error handling

Since 4XX errors are counted towards your request limit, we recommend using a circuit breaker pattern to prevent cascading failures.

For example, if the first request fails, don't perform the second request since it will likely fail too.


# Best Practices

Tips to control Admin API usage and avoid unnecessary costs.

When integrating with our Admin APIs, it’s important to implement them in a way that scales efficiently as your community grows. Rather than relying on frequent polling—which can cause excessive usage, increased costs, and degraded performance—there are more effective ways to build your workflows.

This guide outlines best practices to help you reduce unnecessary Admin API calls and make the most of your integration.

***

### 🔁 Use Built-in Workflows

Our native Workflows feature allows you to build automations that respond to real-time platform events—without writing code or polling the API.

#### Why use workflows?

* React to user or content changes immediately
* Eliminate repetitive, manual API requests
* Configure visually through your admin dashboard

> Example: Trigger a webhook when a new member joins or when a new post is published.

***

### 🔔 Webhooks: The Better Alternative to Polling

When external tools need updates, webhooks are ideal. They notify your system automatically when relevant events occur.

**Benefits of using webhooks:**

* Event-driven and resource-friendly
* Significantly reduces unnecessary API requests
* Easily integrates with webhook-compatible tools

> You can configure webhook actions directly within workflows for seamless automation.

***

### ⚙️ Smarter Polling Strategies (If Necessary)

If you’re using platforms that don’t support webhooks (like certain no-code tools), and polling is your only option, follow these tips:

* ⏱ Increase polling intervals

  (e.g., poll hourly instead of every few minutes)
* 🧭 Stagger requests across endpoints

  Avoid hitting all resources at once—space out your calls.
* ⏰ Limit to active business hours

  Focus on times when meaningful data changes are most likely.

***

### 🧠 Advanced Usage Controls

For more complex or high-traffic integrations, apply these best practices:

* 🗃 Cache responses when possible

  Avoid repeat calls for data that hasn’t changed.
* 🔗 Batch multiple requests into one

  Use supported batch APIs to minimize round-trips.
* 📊 Monitor usage and enforce rate limits

  Add safeguards to prevent unexpected traffic spikes.

***

### 🤝 Zapier: No Usage Cost for Admin v1

For no-code automations, Zapier is a preferred option:

> ✅ Admin v1 API usage through Zapier is excluded from your API quota.

This makes Zapier a great choice for building automations that are:

* Cost-efficient
* Event-driven
* Easy to set up

***

### 🚫 Common Mistakes to Avoid

Avoid these common integration issues:

* ❌ Making repeated or looped API calls for the same data
* ❌ Fetching entire datasets instead of using filters or pagination
* ❌ Polling too frequently without checking for data changes


# Headless

Our Headless offering is designed for communities to integrate Circle features into their own website or app, like discussions, feed, notifications, events, and more.

<figure><img src="/files/5woJ2fRlNiNjaQVYDQ6j" alt=""><figcaption></figcaption></figure>

Available on our Business plan and above:

* [**Member API**](https://api-headless.circle.so/?urls.primaryName=Member%20APIs): A server-side API with endpoints for building your own member-side experiences in your app or website. [See endpoints](https://api-headless.circle.so/?urls.primaryName=Member%20APIs)\
  \
  Unlike the admin API, requests are member-authenticated via the member-specific JWT tokens you'll generate with the [Auth API](https://api-headless.circle.so/).\
  \
  This means every API request is made on behalf of a signed in member on your website or app, allowing you to write your own client-side code for integrating posts, comments, events, notifications, and more into your website or app.<br>
* [**Auth API**](https://api.circle.so/apis/headless/quick-start)**:** A server-side API to authenticate your website or app’s signed in members with the Member API using a JWT token. [See endpoints](https://api-headless.circle.so/)

{% hint style="info" %}
Please note:\
\- The `access_token` persists across sessions, allowing users to possess multiple tokens concurrently.\
\- We do not revoke the`access_token` automatically\
\- Our system automatically revokes the `refresh_token` after one month for enhanced security\
\- If you need selective token revocation, our API offers endpoints dedicated to both `access_token` and `refresh_token` revocation
{% endhint %}

### Security

1. Keep your application token secure by making server-side calls for the Auth API. **Do not expose your admin API token or your application token in your client-side code.**
2. Implement token management for the Member API using JWT access and refresh tokens on the client-side.

For the full list of API endpoints, please visit [https://api-headless.circle.so/.](https://api-headless.circle.so/)

### Feedback

* If you have general questions or want to share your creations with the developer community, please check out our [Developer community space](https://community.circle.so/c/developers/).&#x20;
* If you have API feedback for our engineering team, [please use this form](https://circleco.typeform.com/to/xFEpyITZ#email=xxxxx\&visitor=xxxxx) to reach out to us.


# Quick start

To get started with the Headless APIs, follow these steps.

### Fetch your API token

We use a token based authorization mechanism for both Member and Auth APIs.

Community admins can obtain an API key by going to the **Developers -> Tokens** page in their community and selecting the type as **Headless Auth.**

> IMPORTANT: You will need to use the token type \`Headless Auth\` for it to work with the Auth APIs.

### Auth API

#### Request&#x20;

After generating your API token from the **Developers** tab, you'll need to fetch a signed in member's unique JWT token so you can make requests on their behalf with our [Member API](/apis/headless/member-api).\
\
To authenticate a member and receive the JWT access token, you'll need to pass one of the following params:

* **community\_member\_id:** the member's Circle community member ID
* **email:** the member's email
* **sso\_id:** if you've setup SSO, this will likely be the user ID within your SSO auth system. For instance, a Google ID via Auth0 will look something like `"google-oauth2|106228182038999999999"`

```bash
curl -X POST "https://app.circle.so/api/v1/headless/auth_token" \
     -H "Authorization: Bearer <API_Token>" \
     -H "Content-Type: application/json" \
     -d '{
           "email": "email@circle.co"
         }'
```

#### Response

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiJ9.eyJjb21tdW5pdHlfaWQiOjEsImNvbW11bml0eV9tZW1iZXJfaWQiOjEsInNzb191c2VyX2lkIjoiZjdiOThlYjczZjdkMGQ0NGU0ZWE1MjYyN2JiYjVhMzkiLCJleHAiOjE3MDg1NDE1MTAsImp0aSI6ImE1MjM2ZmQzLWY4NGItNDcyYy1iNjI2LTcyYTk3YmYwZTcyOSJ9.-MY06GiyXB41dLAx_F4Eu8R4sRxq6QEjy3uLWc4Z6k8",
  "refresh_token": "jaebyVK59l5xxAx1D4pM8H-wYyFA6gMC12RGYZcy44w",
  "access_token_expires_at": "2022-01-01T00:00:00.000Z",
  "refresh_token_expires_at": "2022-01-01T00:00:00.000Z",
  "community_member_id": 1,
  "community_id": 1
}
```

This response includes:

1. `access_token`: A JWT token used for authenticating subsequent API requests.
2. `refresh_token`: A token used to obtain a new access token when the current one expires.
3. `access_token_expires_at`: The expiration timestamp for the access token. It expires after 1h.
4. `refresh_token_expires_at`: The expiration timestamp for the refresh token. It expires after 1 month.
5. `community_member_id`: The ID of the community member associated with this token.
6. `community_id`: The ID of the community the member belongs to.

### Member API

Once you've retreived the member's `access_token,` you can make requests on their behalf to the Headless Member API. For example:

```bash
curl -X GET "https://app.circle.so/api/headless/v1/home?page=2&per_page=20&sort=popular" \
     -H "Authorization: Bearer <access_token>" \
     -H "Content-Type: application/json"
     
```

For a full list of member API endpoints, [click here](https://api-headless.circle.so/?urls.primaryName=Member%20APIs).

### Feedback

To reach out to our API engineering team with feedback or requests, [please use this form](https://circleco.typeform.com/to/xFEpyITZ#email=xxxxx\&visitor=xxxxx).


# Member API

The Headless Member API is built for communities with in-house developers to integrate Circle's features into their own website or app.

<figure><img src="/files/qZoiT082OmEqipEiF8zD" alt=""><figcaption></figcaption></figure>

You can use the member API to perform member-authenticated actions for creating and retreiving posts, comments, events, notifications, chat spaces, and more.

For a full list of member API endpoints, [click here](https://api-headless.circle.so/?urls.primaryName=Member%20APIs).

### Feedback

* If you have general questions or want to share your creations with the developer community, please check out our [Developer community space](https://community.circle.so/c/developers/).&#x20;
* If you have API feedback for our engineering team, [please use this form](https://circleco.typeform.com/to/xFEpyITZ#email=xxxxx\&visitor=xxxxx) to reach out to us.


# Community Member Search

This documentation describes using the member filters endpoint to search for community members.

### API Endpoint

```bash
POST /api/headless/v1/search/community_members
```

### Request Parameters

| Parameter                | Type    | Required | Description                                            |
| ------------------------ | ------- | -------- | ------------------------------------------------------ |
| `filters`                | Array   | No       | Array of filter objects                                |
| `search_text`            | String  | No       | Free text search across member profiles                |
| `per_page`               | Integer | No       | Number of results per page                             |
| `search_after`           | Array   | No       | Cursor for pagination beyond 10K records               |
| `order`                  | String  | No       | Sort order: "oldest", "alphabetical", "latest", "role" |
| `status`                 | String  | No       | Filter by status: "active" or "inactive"               |
| `exclude_empty_profiles` | Boolean | No       | When true, excludes profiles with no data              |
| `exclude_empty_name`     | Boolean | No       | When true, excludes profiles with no name              |

### Building Filters

Each filter in the `filters` array follows this structure:

```json
{
  "key": "string",              // Required
  "filter_type": "string",      // Optional
  "profile_field_type": "string", // Optional, for custom fields
  "profile_field_id": "string", // Optional, for custom fields
  "value": "string",           // Optional
  "gte": "integer",            // Optional, for range queries
  "lte": "integer"             // Optional, for range queries
}
```

#### Available Filter Types

* `"is"` - Exact match
* `"is_not"` - Exclude exact match
* `"contains"`  - Partial match
* `"does_not_contain"`  - Exclude partial match
* `"gt"` - Greater than
* `"lt"`  - Less than
* `"eq"` - Equals to

### Filter Examples

#### Basic Profile Fields

```json
{
  "filters": [
    {
      "key": "name",
      "filter_type": "is",
      "value": "John Doe"
    },
    {
      "key": "email",
      "filter_type": "is",
      "value": "john@example.com"
    },
    {
      "key": "headline",
      "filter_type": "contains",
      "value": "developer"
    }
  ]
}
```

#### Custom Profile Fields

```json
{
  "filters": [
    {
      "key": "profile_field",
      "filter_type": "is",
      "profile_field_type": "text",
      "profile_field_id": "123",
      "value": "Software Engineer"
    },
    {
      "key": "profile_field",
      "profile_field_type": "checkbox",
      "profile_field_id": "456",
      "value": "true"
    }
  ]
}
```

#### Activity Score Range

```json
{
  "filters": [
    {
      "key": "activity_score",
      "gte": 5,
      "lte": 7
    }
  ]
}
```

### Cursor-based Pagination

For datasets larger than 10K records, use cursor-based pagination with `search_after`.

#### Initial Request

```json
{
  "filters": [
    {
      "key": "role",
      "value": "Member"
    }
  ],
  "per_page": 20
}
```

#### Response

```json
{
  "data": [...],
  "next_search_after": ["1463538857"],
  "total_count": 15000
}
```

#### Next Page Request

```json
{
  "filters": [
    {
      "key": "role",
      "value": "Member"
    }
  ],
  "per_page": 20,
  "search_after": ["1463538857"]
}
```

### Combining Filters with Pagination

```json
{
  "filters": [
    {
      "key": "name",
      "filter_type": "contains",
      "value": "John"
    },
    {
      "key": "profile_field",
      "filter_type": "is",
      "profile_field_type": "text",
      "profile_field_id": "123",
      "value": "Engineer"
    }
  ],
  "exclude_empty_profiles": true,
  "status": "active",
  "per_page": 20,
  "search_after": ["1463538857"]
}
```

### Best Practices

1. **Filter Combinations**
   * Combine multiple filters to create precise queries
   * Use text filters (`contains`) for broader matches
   * Use exact matches (`is`) for specific fields like email
2. **Pagination**
   * Use `search_after` for datasets larger than 10K records
   * Keep track of the `next_search_after` value for subsequent requests
   * Start a new query without `search_after` if filters change
3. **Performance**
   * Keep filter combinations reasonable
   * Use `search_text` for general searches across all fields
   * Specify exact fields when possible for better performance

### Common Issues

1. **Invalid Search After**
   * If `search_after` becomes invalid, start a new query without it
   * Always use the most recent `next_search_after` value
2. **No More Results**
   * When `next_search_after` is not in the response, you've reached the end
   * Start a new query if you need to change filters
3. **Filter Combinations**
   * Some filter types may not be available for all fields
   * Check the field type before applying specific filters


# Cookies

Use cookies to solve double authentication in iframes and webviews.

{% hint style="info" %}
Disclaimer: Setting cookies in iframes may be affected by browser-specific security policies and privacy settings.
{% endhint %}

Customers who iFrame embed their Circle community in their own app or website commonly face the challenge of needing their members to authenticate twice: once into your own app, and once again into your Circle community via the embedded iFrame.

Using Headless, you can solve this by generating a member token, and injecting session cookies in your website or app so they don't need to authenticate twice.

### SSO (Single Sign-On)

SSO is the best way to integrate user accounts from your platform into Circle. Here's how you can setup a [OAuth0 custom SSO](https://help.circle.so/p/sso-and-integrations/sso/set-up-custom-sso) into your community.&#x20;

For communities that have SSO enabled, Circle stores an attribute called SSO User ID. SSO User ID is an identifier assigned to a user by the SSO  provider. Circle does not create, update, or manage SSO User IDs for SSO-registered members.

Example with Google as the SSO provider: When using Google for authentication, the SSO User ID is typically the user's Google account ID. This is a unique string of numbers and letters that Google assigns to each user account. For instance:

* Google Account ID (SSO User ID): `118234567890123456789`

This ID remains consistent for the user across different services that use Google SSO. Circle would store this ID to associate the user's Google account with their Circle app profile, but Circle does not generate or manage this ID itself.

It's important to note that this ID is **different** from:

* The user's email address (e.g., `user@gmail.com`)
* The user's display name on Google services
* Any internal user ID that Circle might assign for its own database management

{% hint style="info" %}
With SSO configured, it's not necessary to perform any invitation flows (via Admin API or not) via Circle. Once the member is registered into your auth provider, the SSO User ID gets registered in Circle and gets associated with the user's member record.
{% endhint %}

### Injecting session cookies via URL redirect

The simplest way to get authenticated is to navigate to our authentication URL with the access token as part of the query params.

1. Navigate to `https://{communityDomain}/session/cookies?access_token={YOUR_ACCESS_TOKEN}`.
2. That should take you to your community's root path (either `/` or `/home`, depending on your community configuration).

### Injecting session cookies via API

Our cookies endpoint automatically authenticate users in your community iFrame/webview by injecting session cookies into their browser.

To do this, you'll need to make sure that the community domain shares the same top-level TLD as your main website's domain to avoid issues with third-party cookies.

```bash
curl -X POST 'https://{domain}/api/headless/v1/cookies' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json'
```

Once you receive a success (200) response, you'll see `message` parameter with a `Set-Cookies` response header. If you're performing this call using a browser (injecting a fetch request, for instance), the session cookie will automatically be set. If not, you'll need to manually inject the contents of this header as a cookie.

```json
// Response body
{
  "message": "Cookies generated"
}

// Response headers
{
  // ...
  "Set-Cookie": "user_session_identifier={session-identifier-here}%2Fs%2BaSnlgoZ4biUfv3aHG%2FEikGbuYTeO0N4coEf2za7PjyiOd0akeRViuGmFW2I5tZn4igNXANCz2IEJ6x9ATCMlfigJIHJZXU3ZO0T7ohycyXPeS52LDHPMe9Hb3yweV4GEmpfvyJl5wb7A9aszlYzA2Y6zyB6S2DPG2phPUWuv4j8akk6q7MGBp1gIhn2yRYKmA%3D%3D--2DRGZMVYeq5h5JFv--LzMaV5ENIvsfIhnSK5vmlw%3D%3D; path=/; expires=Wed, 10 Sep 2025 13:22:39 GMT; SameSite=None; Secure"
}
```


# Direct upload

To learn more about file uploads, [click here](/get-started/concepts/file-uploads).


# Auth SDK

<figure><img src="/files/aLKFvLp7HQKVO4nsRAy0" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
**The Headless Auth SDK is intended to be used only with server-side apps to authenticate your website/app with your secure App Token. This token should be kept secret, and should not be made available to anyone on the client side for security reasons.**
{% endhint %}


# Getting Started

### Overview

The Circle Auth SDK provides a secure and efficient way to handle member authentication and token management in your server-side applications. The SDK exposes four main functions, allowing you to obtain, refresh, and revoke member access tokens securely.

This SDK was designed to be used on the server to avoid exposing your app token.&#x20;

{% hint style="info" %}
Do not run or expose any of these authentication functions in your client-side codebase.
{% endhint %}

### Generate your API Token

Your `API Token` identifies your community within Circle's server and enables you to perform administrative tasks, such as handling community data or generating new content.&#x20;

Follow [this guide](https://help.circle.so/p/sso-and-integrations/api/create-an-api-token-in-your-community) for instructions on how to generate your API Token.

> IMPORTANT: The API Token should be named \`Headless\` so Auth APIs can work correctly.

### Security Best Practices

* Keep your `API Token` secure by not exposing it to the client side. This SDK was designed to run on the server side to protect sensitive information.
* Securely store members’ `access_token` and `refresh_token` and use them for authenticated requests to our [Headless Member API](https://api-headless.circle.so/?urls.primaryName=Member%20APIs).
* Reinforce the authentication process’ security by refreshing the access tokens regularly.
* Revoking access\_token and refresh\_token whenever a member logs out or when the tokens are no longer needed can prevent unauthorized access.

### Error Handling

Calls to the SDK functions will return a <mark style="color:green;">`Success`</mark> response with the desired data or an error response if something goes wrong. Ensure errors are handled appropriately by providing the user with meaningful error messages whenever necessary.


# Node.js

### Installation

Install `@circleco/headless-server-sdk` to start working with the Authentication SDK.

```bash
npm install @circleco/headless-server-sdk
```

### Import the module into your code

Once the SDK package is installed, import `createClient` from the headless package.

```javascript
import { createClient } from "@circleco/headless-server-sdk";
```

### Create a client

Use the `createClient` function to create a new SDK client instance.

```javascript
const client = createClient({
  appToken: "AppToken", <-- your app token goes here.
});
```

### Send your first request

```typescript
const response = await client.getMemberAPITokenFromSSOId(SSOID);
```


# Methods

The Headless Auth SDK is composed of the following methods:

[getMemberAPITokenFromCommunityMemberId](#getmemberapitokenfromcommunitymemberid)

[getMemberAPITokenFromEmail](#getmemberapitokenfromemail)

[getMemberAPITokenFromSSOId](#getmemberapitokenfromssoid)

[getMemberAPITokenFromRefreshToken](#getmemberapitokenfromrefreshtoken)

[revokeRefreshToken](#revokerefreshtoken)

[revokeMemberAPIToken](#revokememberapitoken)

## getMemberAPITokenFromCommunityMemberId

This function obtains a member `access_token` and `refresh_token` based on the community member's ID.&#x20;

### Request

```
const response = await client.getMemberAPITokenFromCommunityMemberId(MemberID);
```

### Accepted parameters

`memberID` - The Community Member ID. <mark style="color:red;">Required</mark>

### Response

```json5
{
    refresh_token: 'refresh_token',
    refresh_token_expires_at: '2024-04-14T16:39:57.000Z',
    community_member_id: 0000000,
    community_id: 00000,
    access_token: 'access_token',
    access_token_expires_at: '2024-03-20T16:54:47.274Z'
}
```

***

## getMemberAPITokenFromEmail

This function obtains a member `access_token` and `refresh_token` based on the community member's Email.&#x20;

### Request

```
const response = await client.getMemberAPITokenFromEmail(email@community.com)
```

### Accepted parameters

`email` - The Community Member Email. <mark style="color:red;">Required</mark>

### Response

```json5
{
    refresh_token: 'refresh_token',
    refresh_token_expires_at: '2024-04-14T16:39:57.000Z',
    community_member_id: 0000000,
    community_id: 00000,
    access_token: 'access_token',
    access_token_expires_at: '2024-03-20T16:54:47.274Z'
}
```

***

## getMemberAPITokenFromSSOId

This function obtains a member `access_token` and `refresh_token` based on the member's SSO (Single Sign-On) ID.

### Request

```javascript
const response = await client.getMemberAPITokenFromSSOId(SSOID);
```

### Accepted parameters

`SSOID` - The member ID that matches the one in your SSO. <mark style="color:red;">Required</mark>

### Response

```json5
{
    refresh_token: 'refresh_token',
    refresh_token_expires_at: '2024-04-14T16:39:57.000Z',
    community_member_id: 0000000,
    community_id: 00000,
    access_token: 'access_token',
    access_token_expires_at: '2024-03-20T16:54:47.274Z'
}
```

***

## getMemberAPITokenFromRefreshToken

This function obtains a new member `access_token` by providing the member's `refresh_token`. A new token can be generated once the current one is expired.

### Request

```javascript
const response = await client.getMemberAPITokenFromRefreshToken(refreshToken);
```

### Accepted parameters

`refreshToken` - the refresh token received from [`getMemberAPITokenFromSSOId`](#getmemberapitokenfromssoid). <mark style="color:red;">Required</mark>

### Response

```json5
{
    access_token: "newAccessToken",
    access_token_expires_at: "2024-03-20T16:55:50.727Z"
}
```

***

## revokeRefreshToken

Revokes a member's `refresh_token`, making it unusable for generating new `access_token`.

### Request

```javascript
const response = await client.revokeRefreshToken(refreshToken);
```

### Accepted parameters

`refreshToken` - the refresh token to be revoked. <mark style="color:red;">Required</mark>

### Response

on a successful request

```json5
null
```

on a failed request

```json5
{
    success: false,
    message: "Message"
}
```

***

## revokeMemberAPIToken

Revokes a member's `access_token`, making it unusable for future authenticated requests.

### Request

```javascript
const response = await client.revokeMemberAPIToken(accessToken);
```

### Accepted parameters

`accessToken` - the member access token to be revoked. <mark style="color:red;">Required</mark>

### Response

on a successful request

```json5
null
```

on a failed request

```json5
{
    success: false,
    message: "Message"
}
```


# Ruby

Our Ruby Auth SDK will be available in the coming months. For now, please refer to the [Swagger API docs](https://api-headless.circle.so/?urls.primaryName=Member%20APIs) for a backend-agnostic approach. If you need any help, please contact the Headless team.


# Go

Our Go Auth SDK will be available in the coming months. For now, please refer to the [Swagger API docs](https://api-headless.circle.so/?urls.primaryName=Member%20APIs) for a backend-agnostic approach. If you need any help, please contact the Headless team.


# Python

Our Python Auth SDK will be available in the coming months. For now, please refer to the [Swagger API docs](https://api-headless.circle.so/?urls.primaryName=Member%20APIs) for a backend-agnostic approach. If you need any help, please contact the Headless team.


# Usage and limits

For Headless APIs, the usage is based on MAUs (Monthly Active Users). \
\
MAU is the number of unique users (with uniqueness defined by the user-ID) that create or consume content on Circle servers through SDK or API within a monthly billing cycle. All service requests except authentication\* are counted as MAU. MAU number is based on per-user instead of per-device, meaning that a user with the same user-ID with multiple devices will be counted as 1 MAU.

### Exceptions

Authentication endpoints are not taken into account, so you can use them without increasing your MAU count. Here's a list of what we consider Auth endpoints:

* /api/v1/headless/access\_token/refresh
* /api/v1/headless/access\_token/revoke
* /api/v1/headless/auth\_token
* /api/v1/headless/refresh\_token/revoke
* /api/headless/v1/cookies
* /session/cookies (cookies redirect URL)


# Data API

<figure><img src="/files/WmXOgAmqM0Ny5ZiMH1a2" alt=""><figcaption></figcaption></figure>

### What is the Data API?

Circle's Data API provides a comprehensive set of events to track various actions and changes within your community. [See endpoints](https://app.circle.so/data_api/docs)

### Who is this for?

Available on our **Plus Platform plan** and above, the Data API is invaluable for analysts, product/course managers, and business leaders looking to analyze their community and business.

By importing all their community data into their own data warehouse, the Data API provides the ability to develop custom reports and combine your community data with other sources to get a more holistic view of your business.

### Key benefits

1. **Up-to-date analytics**: Track user actions and community updates as they happen in your own data stack.
2. **Enhanced reporting**: Build custom reporting and dashboards using your own BI tool.
3. **Custom Integrations**: Integrate your Circle data with other tools and services to gain a deeper understanding of your business.

### What can I do with it that wasn't possible before?

Without the Data API, you can access data and insights from your Circle community in two ways:

1. **In-product dashboards:** these provide an excellent standard set of insights to help you understand community performance and stay on top of major trends. However, many of our highly advanced community owners need more depth and flexibility to track metrics that are specific to their business and objectives. For these customers, until now, the only other option has been to take CSV exports of their community data
2. **CSV Exports:** this option gives you more control and allows you to study your data at an extremely granular level. However, CSV exports can’t be automated and require manual work to ingest this data for analysis over and over again, and they don’t cover all the use cases that the Data API is designed for.

Some of our advanced users with programming skills or access to developers on their team also use our Admin API to extract some data. However, the main purpose of these API is to administer your community and automate actions, not to perform analysis on your Circle data. To repurpose the Admin API to produce data is cumbersome, and requires clever scripting at the customer’s end to get around its limitations.

To address this need and supercharge your analytics, we've designed the Data API to give you a high-throughput, purpose-driven interface to all your Circle data, which you can use to plug into your own data warehouse with an ETL tool, such as Airbyte.

### Requirements

#### **Technical Requirements**

We recommend using the Data API to store data in a Data Warehouse where you can transform it into any desired data models and metrics for tracking and analysis. We have made the API compatible with ETL tools such as Airbyte (recommended) or Fivetran. A typical set up this API could work as follows:

<figure><img src="/files/xncZtrvK2co9SvFgRsvo" alt=""><figcaption></figcaption></figure>

**Expertise required**

On your end, you'll need a Data Analyst or an individual who can connect the Data API via an ETL tool and store it in a data warehouse. Once connected, they will need to write SQL queries on top of the gathered data to generate custom metrics and insights.

### How do I get access?

If you're interested, please use this link to request a time to speak with our sales team:

&#x20;👉  [Schedule a Meeting to Learn about the Data API](https://circle.so/plus-sales)

### [See docs](https://app.circle.so/data_api/docs)


# Circle Plus


# Deep Linking Guide

Use deep links to send users directly to specific content inside the Circle mobile app — from push notifications, emails, or any external surface.

All routes support both the **app scheme** and **universal links** (HTTPS).

| Format         | Example                                     |
| -------------- | ------------------------------------------- |
| App scheme     | `yourapp://messages`                        |
| Universal link | `https://yourcommunity.domain.com/messages` |

***

### Routes

#### Home & Navigation

| URL                       | Opens                |
| ------------------------- | -------------------- |
| `yourapp://`              | Home feed            |
| `yourapp://search`        | Search               |
| `yourapp://notifications` | Notifications        |
| `yourapp://messages`      | Direct messages list |

***

#### Spaces & Content

| URL                                           | Opens              |
| --------------------------------------------- | ------------------ |
| `yourapp://c/:spaceSlug`                      | A space            |
| `yourapp://c/:spaceSlug/:postSlug`            | A specific post    |
| `yourapp://c/:spaceSlug/:postSlug/:commentId` | A specific comment |

**Course spaces:**

| URL                                                                     | Opens                       |
| ----------------------------------------------------------------------- | --------------------------- |
| `yourapp://c/:spaceSlug/sections/:sectionId/lessons/:lessonId`          | A course lesson             |
| `yourapp://c/:spaceSlug/sections/:sectionId/lessons/:lessonId/comments` | Comments on a course lesson |

***

#### Direct Messages

| URL                                                                 | Opens                                       |
| ------------------------------------------------------------------- | ------------------------------------------- |
| `yourapp://messages/:chatRoomUuid`                                  | A chat room                                 |
| `yourapp://messages/:chatRoomUuid?message_id=X`                     | A chat room, scrolled to a specific message |
| `yourapp://messages/:chatRoomUuid?thread_message_id=X&message_id=Y` | A chat room with a thread open              |
| `yourapp://messages/:chatRoomUuid/invite_member`                    | A chat room (user is auto-joined)           |

***

#### Members

| URL                     | Opens              |
| ----------------------- | ------------------ |
| `yourapp://u/:memberId` | A member's profile |
| `yourapp://connections` | Connections panel  |

***

#### Other

| URL                                    | Opens                         |
| -------------------------------------- | ----------------------------- |
| `yourapp://join`                       | Join via invitation link      |
| `yourapp://live/:liveName`             | A live stream                 |
| `yourapp://checkout/:id`               | A paywall / checkout page     |
| `yourapp://cab/screen/:orphanScreenId` | A custom page (orphan screen) |

***

### Parameters

| Parameter           | Used in                          | Description                                                  |
| ------------------- | -------------------------------- | ------------------------------------------------------------ |
| `message_id`        | Messages, course lesson comments | Scrolls to a specific message                                |
| `thread_message_id` | Messages                         | Opens a thread at a specific message (use with `message_id`) |

***

### URL Parameters Reference

Replace placeholders with real values:

| Placeholder       | What to use                                        |
| ----------------- | -------------------------------------------------- |
| `:spaceSlug`      | The space's slug (visible in the space URL on web) |
| `:postSlug`       | The post's slug (visible in the post URL on web)   |
| `:commentId`      | The comment's numeric ID                           |
| `:chatRoomUuid`   | The chat room's UUID                               |
| `:memberId`       | The member's numeric ID                            |
| `:sectionId`      | The course section ID                              |
| `:lessonId`       | The course lesson ID                               |
| `:liveName`       | The live stream name/slug                          |
| `:id`             | The paywall/checkout ID                            |
| `:orphanScreenId` | The custom page (orphan screen) ID                 |

***

### Examples

```
# Open a post
yourapp://c/announcements/welcome-to-the-community

# Open a course lesson
yourapp://c/foundations-course/sections/12/lessons/34

# Open a DM and scroll to a message
yourapp://messages/a1b2c3d4-e5f6-7890-abcd-ef1234567890?message_id=9876543

# Open a member profile
yourapp://u/42
```


# Custom HTML API reference

This document describes the JavaScript API available to custom HTML content rendered inside the Circle Plus mobile app. Use these functions and objects to interact with the native app from your custom HTML screens.

### Overview

When your custom HTML content is loaded inside the Circle mobile app, several JavaScript functions and objects are automatically injected into the `window` object. These allow you to:

* Access information about the currently authenticated user
* Navigate to other screens within the app
* Open URLs through the app's navigation system
* Send custom messages to the native app

All injected code is available immediately when your HTML loads - no additional setup required.

***

### Global Objects

#### window\.circleUser

An object containing information about the currently authenticated user.

**Type:** `Object`

**Properties:**

| Property      | Type      | Description                                 |
| ------------- | --------- | ------------------------------------------- |
| `name`        | `string`  | User's full display name                    |
| `email`       | `string`  | User's email address                        |
| `publicUid`   | `string`  | User's unique public identifier             |
| `isAdmin`     | `boolean` | `true` if the user has admin privileges     |
| `isModerator` | `boolean` | `true` if the user has moderator privileges |

**Example:**

```javascript
// Access current user information
console.log(window.circleUser);
// Output:
// {
//   "name": "John Doe",
//   "email": "john@example.com",
//   "publicUid": "abc123xyz",
//   "isAdmin": false,
//   "isModerator": true
// }

// Personalize content based on user
document.getElementById('greeting').textContent = `Welcome, ${window.circleUser.name}!`;

// Show admin-only content
if (window.circleUser.isAdmin) {
  document.getElementById('admin-panel').style.display = 'block';
}
```

***

### Navigation Functions

#### navigateToOrphanedScreen()

Navigates to another custom (orphaned/not in nav) screen within the app by its screen ID. This will "stop" the native navigation inside the webview.

**Signature:**

```javascript
window.navigateToOrphanedScreen(screenId)
```

**Parameters:**

| Parameter  | Type     | Required | Description                                                 |
| ---------- | -------- | -------- | ----------------------------------------------------------- |
| `screenId` | `string` | Yes      | The unique identifier of the orphaned screen to navigate to |

**Returns:** `void`

**Example:**

```javascript
// Navigate to a specific custom screen
window.navigateToOrphanedScreen('9a581ef4-8a0e-432b-8a83-2885c9b4e83a');

// Use in a button click handler
document.getElementById('next-btn').addEventListener('click', function() {
  window.navigateToOrphanedScreen('9a581ef4-8a0e-432b-8a83-2885c9b4e83a');
});
```

**Notes:**

* The screen ID must correspond to an existing orphaned screen in your Circle community
* Navigation is handled natively, providing smooth transitions

***

#### navigateToUrl()

Opens a URL through the app's navigation system. The app will handle the URL appropriately based on its type (deep links, external URLs, etc.).

**Signature:**

```javascript
window.navigateToUrl(url)
```

**Parameters:**

| Parameter | Type     | Required | Description            |
| --------- | -------- | -------- | ---------------------- |
| `url`     | `string` | Yes      | The URL to navigate to |

**Returns:** `void`

**Example:**

```javascript
// Navigate to an external website
window.navigateToUrl('https://example.com/resource');

// Navigate using a Circle deep link
window.navigateToUrl('https://community.circle.so/c/announcements');

// Use in a link click handler
document.querySelectorAll('a.native-nav').forEach(function(link) {
  link.addEventListener('click', function(e) {
    e.preventDefault();
    window.navigateToUrl(this.href);
  });
});
```

**Notes:**

* Deep links to Circle content will navigate within the app
* External URLs may open in an in-app browser or external browser depending on app configuration

### Feature Detection

You can check if your HTML is running inside the Circle mobile app:

```javascript
// Check if isInsideCircleMobileWebview is available
if (window.isInsideCircleMobileWebview) {
  // User data is available
  console.log('Logged in as:', window.circleUser.name);
}
```

***

### Callback Functions

The Circle mobile app provides callback functions that your custom HTML can call to notify the native app about specific events. These are particularly useful for signup flows, content creation, and payment processes.

#### window\.webview\.accountCreatedCallback()

Called when a user account is successfully created. This function notifies the native app and may clear cookies.

**Signature:**

```javascript
window.webview.accountCreatedCallback(data)
```

**Parameters:**

| Parameter | Type     | Required | Description                                                   |
| --------- | -------- | -------- | ------------------------------------------------------------- |
| `data`    | `object` | Yes      | Account creation data including email and profile information |

**Example:**

```javascript
// After successful account creation
window.webview.accountCreatedCallback({
  email: 'newuser@example.com'
});
```

**Notes:**

* This function is typically available only on signup/registration screens
* May automatically clear cookies after being called

***

#### window\.webview\.handleEmailAlreadyExistsError()

Handles the case when a user tries to sign up with an email that already exists. This modifies the error link behavior to communicate with the native app instead of navigating.

**Signature:**

```javascript
window.webview.handleEmailAlreadyExistsError()
```

**Parameters:** None

**Example:**

```javascript
// Call this when the signup form shows an "email already exists" error
if (window.webview.handleEmailAlreadyExistsError) {
  window.webview.handleEmailAlreadyExistsError();
}
```

**Notes:**

* This function modifies the DOM to intercept clicks on the existing account error link
* Sends an error message to the native app instead of navigating

#### window\.webview\.onProfileInfoSaved()

Called when user profile information has been successfully saved. Triggers navigation back and shows a success notification.

**Signature:**

```javascript
window.webview.onProfileInfoSaved()
```

**Parameters:** None

**Example:**

```javascript
// After successfully saving profile changes
if (window.webview.onProfileInfoSaved) {
  window.webview.onProfileInfoSaved();
}
```

***

#### window\.webview\.onEventPurchaseEnded()

Called when an event purchase flow is completed. Used in paywall screens.

**Signature:**

```javascript
window.webview.onEventPurchaseEnded()
```

**Parameters:** None

**Example:**

```javascript
// After event purchase completion
if (window.webview.onEventPurchaseEnded) {
  window.webview.onEventPurchaseEnded();
}
```

***

#### window\.webview\.paymentSuccessful()

Called when a payment is successfully processed. Used in paywall and billing screens.

**Signature:**

```javascript
window.webview.paymentSuccessful()
```

**Parameters:** None

**Example:**

```javascript
// After successful payment processing
if (window.webview.paymentSuccessful) {
  window.webview.paymentSuccessful();
}
```

***

### Theme and Appearance

#### window\.webview\.setAppearance()

Allows your custom HTML to switch between light and dark themes to match the native app's appearance.

**Signature:**

```javascript
window.webview.setAppearance(appearance)
```

**Parameters:**

| Parameter    | Type     | Required | Description              |
| ------------ | -------- | -------- | ------------------------ |
| `appearance` | `string` | Yes      | Either "light" or "dark" |

**Example:**

```javascript
// Set dark mode
if (window.webview.setAppearance) {
  window.webview.setAppearance("dark");
}

// Match system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (window.webview.setAppearance) {
  window.webview.setAppearance(prefersDark ? "dark" : "light");
}
```

### Examples

#### Personalized Welcome Screen

```html
<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      padding: 20px;
      margin: 0;
    }
    .welcome {
      text-align: center;
      padding: 40px 20px;
    }
    .user-name {
      font-size: 24px;
      font-weight: bold;
      margin-bottom: 10px;
    }
    .badge {
      display: inline-block;
      padding: 4px 12px;
      border-radius: 12px;
      font-size: 12px;
      margin: 4px;
    }
    .badge-admin { background: #e3f2fd; color: #1976d2; }
    .badge-moderator { background: #f3e5f5; color: #7b1fa2; }
    .nav-button {
      display: block;
      width: 100%;
      padding: 16px;
      margin: 10px 0;
      border: none;
      border-radius: 8px;
      background: #6366f1;
      color: white;
      font-size: 16px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div class="welcome">
    <div class="user-name" id="userName"></div>
    <div id="badges"></div>
  </div>

  <button class="nav-button" onclick="goToResources()">
    View Resources
  </button>

  <button class="nav-button" onclick="goToHelp()">
    Get Help
  </button>

  <script>
    // Personalize the welcome message
    document.getElementById('userName').textContent =
      'Welcome, ' + window.circleUser.name + '!';

    // Show role badges
    var badgesHtml = '';
    if (window.circleUser.isAdmin) {
      badgesHtml += '<span class="badge badge-admin">Admin</span>';
    }
    if (window.circleUser.isModerator) {
      badgesHtml += '<span class="badge badge-moderator">Moderator</span>';
    }
    document.getElementById('badges').innerHTML = badgesHtml;

    // Navigation functions
    function goToResources() {
      window.navigateToOrphanedScreen('resources-screen');
    }

    function goToHelp() {
      window.navigateToUrl('https://help.example.com');
    }
  </script>
</body>
</html>
```

#### Multi-Step Form Navigation

```html
<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
  <h1>Step 1: Basic Information</h1>

  <form id="step1Form">
    <input type="text" id="company" placeholder="Company Name" required>
    <button type="submit">Continue to Step 2</button>
  </form>

  <script>
    document.getElementById('step1Form').addEventListener('submit', function(e) {
      e.preventDefault();

      // Save data to localStorage for next step
      localStorage.setItem('company', document.getElementById('company').value);

      // Navigate to step 2
      window.navigateToOrphanedScreen('onboarding-step-2');
    });
  </script>
</body>
</html>
```

#### Conditional Content Based on User Role

```html
<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
  <div id="member-content">
    <h1>Member Dashboard</h1>
    <p>Welcome to your dashboard!</p>
  </div>

  <div id="admin-content" style="display: none;">
    <h2>Admin Tools</h2>
    <button onclick="window.navigateToOrphanedScreen('admin-settings')">
      Manage Settings
    </button>
    <button onclick="window.navigateToOrphanedScreen('user-management')">
      Manage Users
    </button>
  </div>

  <script>
    // Show admin tools only for admins
    if (window.circleUser.isAdmin) {
      document.getElementById('admin-content').style.display = 'block';
    }
  </script>
</body>
</html>
```

***

### Best Practices

#### 1. Always Check for API Availability

```javascript
// Wrap calls in availability checks for graceful degradation
if (typeof window.navigateToOrphanedScreen === 'function') {
  window.navigateToOrphanedScreen('9a581ef4-8a0e-432b-8a83-2885c9b4e83a');
} else {
  // Fallback for browser testing
  console.log('Would navigate to: my-screen');
}
```

#### 2. Use Viewport Meta Tag

Include the viewport meta tag for proper mobile rendering:

```html
<meta name="viewport" content="width=device-width, initial-scale=1.0">
```

#### 3. Handle Missing User Data Gracefully

```javascript
var userName = (window.circleUser && window.circleUser.name) || 'Guest';
```

#### 4. Test in Browser During Development

Create mock objects for browser testing but make sure to also test on your app, webviews can behave differently than a regular browser.

```javascript
// Add this at the top of your script for browser testing
if (typeof window.circleUser === 'undefined') {
  window.circleUser = {
    name: 'Test User',
    email: 'test@example.com',
    publicUid: 'test-123',
    isAdmin: true,
    isModerator: false
  };
}

if (typeof window.navigateToOrphanedScreen === 'undefined') {
  window.navigateToOrphanedScreen = function(screenId) {
    console.log('Navigate to screen:', screenId);
  };
}

if (typeof window.navigateToUrl === 'undefined') {
  window.navigateToUrl = function(url) {
    console.log('Navigate to URL:', url);
    window.location.href = url;
  };
}
```

#### 5. Use Mobile-Friendly Styles

```css
/* Ensure touch targets are large enough */
button, a {
  min-height: 44px;
  min-width: 44px;
}

/* Use system fonts for native feel */
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/* Prevent text selection issues */
* {
  -webkit-user-select: none;
  user-select: none;
}

/* Allow text selection in input fields */
input, textarea {
  -webkit-user-select: auto;
  user-select: auto;
}
```

***

### API Summary

| API                                           | Type     | Description                |
| --------------------------------------------- | -------- | -------------------------- |
| `window.circleUser`                           | Object   | Current user information   |
| `window.circleUser.name`                      | string   | User's display name        |
| `window.circleUser.email`                     | string   | User's email               |
| `window.circleUser.publicUid`                 | string   | User's public ID           |
| `window.circleUser.isAdmin`                   | boolean  | Admin status               |
| `window.circleUser.isModerator`               | boolean  | Moderator status           |
| `window.navigateToOrphanedScreen(screenId)`   | Function | Navigate to custom screen  |
| `window.navigateToUrl(url)`                   | Function | Navigate to URL            |
| `window.ReactNativeWebView.postMessage(json)` | Function | Send message to native app |

***

### Support

If you encounter issues with the Custom HTML API, please contact the Circle support team or your community administrator.


