Actions API

Overview

Actions represent social interactions and activities in Cloudillo. This includes posts, comments, reactions, connections, and more.

The Actions API allows you to:

  • Create posts, comments, and reactions
  • Manage user connections and follows
  • Share files and resources
  • Send messages
  • Track action statistics

Action Types

Content Actions

Type Description Audience Examples
POST Create a post or content Followers, Public, Custom Blog posts, status updates
CMNT Comment on an action Parent action audience Thread replies
REPOST Share existing content Followers Retweets
SHRE Share resource/link Followers, Custom Link sharing

User Actions

Type Description Audience Examples
FLLW Follow a user/community Target user Subscribe to updates
CONN Connection request Target user Friend requests

Communication Actions

Type Description Audience Examples
MSG Private message Specific user Direct messages
FSHR File sharing Specific user(s) Share documents

Metadata Actions

Type Description Audience Examples
REACT React to content None (broadcast) Likes, loves
ACK Acknowledgment Parent action issuer Read receipts

Endpoints

List Actions

GET /api/actions

Query all actions with optional filters. Uses cursor-based pagination for stable results.

Authentication: Optional (visibility-based access control)

Query Parameters:

Filtering:

  • type - Filter by action type(s), comma-separated (e.g., POST,CMNT)
  • status - Filter by status(es), comma-separated (P=Pending, A=Active, D=Deleted, C=Created, N=New)
  • issuer - Filter by issuer identity (e.g., alice@example.com)
  • audience - Filter by audience identity
  • parentId - Filter by parent action (for comments/reactions)
  • rootId - Filter by root/thread ID (for nested comments)
  • subject - Filter by subject identity (for CONN, FLLW actions)
  • involved - Filter by actions involving a specific identity (issuer, audience, or subject)
  • actionId - Filter by specific action ID
  • tag - Filter by content tag

Time-based:

  • createdAfter - ISO 8601 timestamp or Unix seconds

Pagination:

  • limit - Max results (default: 20)
  • cursor - Opaque cursor for next page (from previous response)

Sorting:

  • sort - Sort field: created (default)
  • sortDir - Sort direction: asc or desc (default: desc)

Examples:

Get recent posts:

const api = cloudillo.createApiClient()

const posts = await api.actions.list({
  type: 'POST',
  status: 'A',
  limit: 20,
  sort: 'created',
  sortDir: 'desc'
})

Get comments on a specific post:

const comments = await api.actions.list({
  type: 'CMNT',
  parentId: 'act_post123',
  sort: 'created',
  sortDir: 'asc'
})

Get all actions involving a specific user:

const userActivity = await api.actions.list({
  involved: 'alice@example.com',
  limit: 50
})

Get posts from a time range:

const lastWeek = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()

const recentPosts = await api.actions.list({
  type: 'POST',
  status: 'A',
  createdAfter: lastWeek,
  limit: 100
})

Get pending connection requests:

const connectionRequests = await api.actions.list({
  type: 'CONN',
  status: 'P',
  subject: cloudillo.idTag // Requests to me
})

Get thread with all nested comments:

const thread = await api.actions.list({
  rootId: 'act_original_post',
  type: 'CMNT',
  sort: 'created',
  sortDir: 'asc',
  limit: 200
})

Cursor-based pagination:

// First page
const page1 = await api.actions.list({ type: 'POST', limit: 20 })

// Next page using cursor
if (page1.cursorPagination?.hasMore) {
  const page2 = await api.actions.list({
    type: 'POST',
    limit: 20,
    cursor: page1.cursorPagination.nextCursor
  })
}

Response:

{
  "data": [
    {
      "actionId": "act_abc123",
      "type": "POST",
      "issuer": {
        "idTag": "alice@example.com",
        "name": "Alice Johnson",
        "profilePic": "/file/b1~abc"
      },
      "content": {
        "text": "Hello, Cloudillo!",
        "title": "My First Post"
      },
      "createdAt": "2025-01-01T12:00:00Z",
      "visibility": "P",
      "stat": {
        "reactions": 5,
        "comments": 3,
        "ownReaction": "LOVE"
      }
    }
  ],
  "cursorPagination": {
    "nextCursor": "eyJzIjoiY3JlYXRlZCIsInYiOjE3MzUwMDAwMDAsImlkIjoiYWN0X2FiYzEyMyJ9",
    "hasMore": true
  },
  "time": "2025-01-01T12:00:00Z"
}

Create Action

POST /api/actions

Create a new action (post, comment, reaction, etc.).

Authentication: Required

Request Body:

interface NewAction {
  type: string // Action type (POST, CMNT, etc.)
  subType?: string // Optional subtype/category
  parentId?: string // For comments, reactions
  rootId?: string // For deep threads
  audienceTag?: string // Target audience
  content?: unknown // Action-specific content
  attachments?: string[] // File IDs
  subject?: string // Target (e.g., who to follow)
  expiresAt?: number // Expiration timestamp
}

Examples:

Create a Post:

const post = await api.actions.create({
  type: 'POST',
  content: {
    text: 'Hello, Cloudillo!',
    title: 'My First Post'
  },
  attachments: ['file_123', 'file_456']
})

Create a Comment:

const comment = await api.actions.create({
  type: 'CMNT',
  parentId: 'act_post123',
  content: {
    text: 'Great post!'
  }
})

Create a Reaction:

const reaction = await api.actions.create({
  type: 'REACT',
  subType: 'LOVE',
  parentId: 'act_post123'
})

Follow a User:

const follow = await api.actions.create({
  type: 'FLLW',
  subject: 'bob@example.com'
})

Connect with a User:

const connection = await api.actions.create({
  type: 'CONN',
  subject: 'bob@example.com',
  content: {
    message: 'Would like to connect!'
  }
})

Share a File:

const share = await api.actions.create({
  type: 'FSHR',
  subject: 'bob@example.com',
  attachments: ['file_789'],
  content: {
    permission: 'READ', // or 'WRITE'
    message: 'Check out this document'
  }
})

Response:

{
  "data": {
    "actionId": "act_newpost123",
    "type": "POST",
    "issuerTag": "alice@example.com",
    "content": {
      "text": "Hello, Cloudillo!",
      "title": "My First Post"
    },
    "createdAt": 1735000000,
    "status": "A"
  }
}

Get Action

GET /api/actions/:actionId

Retrieve a specific action by ID.

Example:

const action = await api.actions.get('act_123')

Response:

{
  "data": {
    "actionId": "act_123",
    "type": "POST",
    "issuerTag": "alice@example.com",
    "issuer": {
      "idTag": "alice@example.com",
      "name": "Alice Johnson",
      "profilePic": "/file/b1~abc"
    },
    "content": {
      "text": "Hello!",
      "title": "Greeting"
    },
    "createdAt": 1735000000,
    "stat": {
      "reactions": 10,
      "comments": 5,
      "ownReaction": null
    }
  }
}
Actions are Immutable

Actions are signed JWTs and cannot be modified after creation. There is no PATCH endpoint for actions. To “edit” content, delete the original and create a new action.

Delete Action

DELETE /api/actions/:actionId

Delete an action. Only the issuer can delete their actions.

Authentication: Required (must be issuer)

Example:

await api.actions.delete('act_123')

Response:

{
  "data": "ok"
}

Accept Action

POST /api/actions/:actionId/accept

Accept a pending action (e.g., connection request, follow request).

Authentication: Required (must be the subject/target)

DSL Hooks: When an action is accepted, the server triggers the on_accept hook defined in the action type’s DSL configuration. This can execute custom logic such as:

  • Creating reciprocal connections (CONN actions)
  • Adding the user to groups (INVT actions)
  • Granting permissions (FSHR actions)

Example:

// Accept a connection request
await api.actions.accept('act_connreq123')

// Accept a follow request (for private accounts)
await api.actions.accept('act_follow456')

Response:

{
  "data": {
    "actionId": "act_connreq123",
    "status": "A"
  }
}

Reject Action

POST /api/actions/:actionId/reject

Reject a pending action.

Authentication: Required (must be the subject/target)

DSL Hooks: When an action is rejected, the server triggers the on_reject hook defined in the action type’s DSL configuration. This can execute cleanup logic such as:

  • Removing pending permissions
  • Notifying the issuer of the rejection
  • Cleaning up temporary resources

Example:

await api.actions.reject('act_connreq123')

Response:

{
  "data": {
    "actionId": "act_connreq123",
    "status": "D"
  }
}

Update Statistics

POST /api/actions/:actionId/stat

Update action statistics (typically called by the system).

Authentication: Required (admin)

Request Body:

{
  reactions?: number
  comments?: number
  views?: number
}

Add Reaction

POST /api/actions/:actionId/reaction

Add a reaction to an action. Creates a REACT action.

Authentication: Required

Request Body:

{
  type: string // Reaction type (e.g., 'LOVE')
}

Example:

await api.actions.addReaction('act_post123', {
  type: 'LOVE'
})

Response:

{
  "data": {
    "actionId": "act_reaction789",
    "type": "REACT",
    "subType": "LOVE",
    "parentId": "act_post123",
    "issuerTag": "alice@example.com"
  }
}

Federation Inbox

POST /api/inbox

Receive federated actions from other Cloudillo instances. This is the primary endpoint for cross-instance action delivery. Processing is asynchronous.

Authentication: Not required (actions are verified via signatures)

Request Body: Action token (JWT)

Example:

// This is typically called by other Cloudillo servers
const actionToken = 'eyJhbGc...' // Signed action token

const response = await fetch('/api/inbox', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/jwt'
  },
  body: actionToken
})

Response:

{
  "data": {
    "actionId": "act_federated123",
    "status": "received",
    "verified": true
  },
  "time": "2025-01-01T12:00:00Z"
}

Federation Inbox (Synchronous)

POST /api/inbox/sync

Receive federated actions with synchronous processing. Unlike the standard inbox, this endpoint processes the action immediately and returns the result.

Authentication: Not required (actions are verified via signatures)

Request Body: Action token (JWT)

Response:

{
  "data": {
    "actionId": "act_federated123",
    "status": "processed",
    "verified": true
  },
  "time": "2025-01-01T12:00:00Z"
}
Info

Both inbox endpoints verify the action signature against the issuer’s public key before accepting it.

Action Status Flow

Actions have a lifecycle represented by status:

C (Created) → A (Active/Accepted)
            ↘ D (Deleted)

P (Pending) → A (Active)
            ↘ D (Deleted)
  • P (Pending): Draft or unpublished content
  • A (Active): Published, visible, and finalized
  • D (Deleted): Soft-deleted or rejected
  • C (Created): Awaiting acceptance (e.g., connection requests)
  • N (New): Notification awaiting acknowledgment

Status transitions:

  • Only Created or Pending actions can be accepted/rejected
  • Only Pending actions can be updated before publishing
  • Any action can be deleted by issuer (changes status to Deleted)
  • Accepting changes status to Active
  • Rejecting changes status to Deleted

Content Schemas

Different action types have different content structures:

POST Content

{
  title?: string
  text?: string
  summary?: string
  category?: string
  tags?: string[]
}

CMNT Content

{
  text: string
}

MSG Content

{
  text: string
  subject?: string
}

FSHR Content

{
  permission: 'READ' | 'WRITE'
  message?: string
  expiresAt?: number
}

CONN Content

{
  message?: string
}

Action Statistics

Actions can have aggregated statistics:

interface ActionStat {
  reactions?: number // Total reactions
  comments?: number // Total comments
  commentsRead?: number // Comments user has read
  ownReaction?: string // User's own reaction type
  views?: number // View count
  shares?: number // Share count
}

Accessing statistics:

const action = await api.actions.get('act_123')

console.log('Reactions:', action.stat?.reactions)
console.log('Comments:', action.stat?.comments)
console.log('My reaction:', action.stat?.ownReaction)

Threading

Actions support threading via parentId and rootId:

POST (root action)
└── CMNT (comment)
    └── CMNT (reply to comment)
        └── CMNT (nested reply)

Creating a thread:

// Original post
const post = await api.actions.create({
  type: 'POST',
  content: { text: 'Main post' }
})

// First level comment
const comment1 = await api.actions.create({
  type: 'CMNT',
  parentId: post.actionId,
  rootId: post.actionId,
  content: { text: 'A comment' }
})

// Reply to comment
const reply = await api.actions.create({
  type: 'CMNT',
  parentId: comment1.actionId,
  rootId: post.actionId, // Still points to original post
  content: { text: 'A reply' }
})

Fetching a thread:

// Get all comments in a thread
const thread = await api.actions.list({
  rootId: 'act_post123',
  type: 'CMNT',
  sort: 'created',
  sortDir: 'asc'
})

Federation

Actions are the core of Cloudillo’s federation model. Each action is cryptographically signed and can be verified across instances.

Action Token Structure:

Header: { alg: "ES384", typ: "JWT" }
Payload: {
  actionId: "act_123",
  type: "POST",
  issuerTag: "alice@example.com",
  content: {...},
  createdAt: 1735000000,
  iat: 1735000000,
  exp: 1735086400
}
Signature: <ES384 signature>

This enables:

  • Trust-free verification
  • Cross-instance action delivery
  • Tamper-proof audit trails

Best Practices

1. Use Cursor-Based Pagination

// ✅ Use cursor pagination for stable results
async function fetchAllPosts() {
  const allPosts = []
  let cursor = undefined

  while (true) {
    const result = await api.actions.list({
      type: 'POST',
      limit: 50,
      cursor
    })

    allPosts.push(...result.data)

    if (!result.cursorPagination?.hasMore) break
    cursor = result.cursorPagination.nextCursor
  }

  return allPosts
}

2. Optimistic UI Updates

// Update UI immediately, rollback on error
const optimisticAction = {
  actionId: 'temp_' + Date.now(),
  type: 'POST',
  content: { text: 'New post' },
  issuer: { idTag: cloudillo.idTag },
  createdAt: new Date().toISOString()
}

setPosts([optimisticAction, ...posts])

try {
  const created = await api.actions.create(optimisticAction)
  setPosts(posts => posts.map(p =>
    p.actionId === optimisticAction.actionId ? created : p
  ))
} catch (error) {
  setPosts(posts => posts.filter(p =>
    p.actionId !== optimisticAction.actionId
  ))
}

3. Handle Visibility

Actions have visibility levels that control who can see them:

Code Level Description
P Public Anyone can view
V Verified Authenticated users only
F Follower User’s followers only
C Connected Mutual connections only
null Direct Owner + explicit audience
// Create a public post
const publicPost = await api.actions.create({
  type: 'POST',
  visibility: 'P',
  content: { text: 'Hello everyone!' }
})

// Create a followers-only post
const followersPost = await api.actions.create({
  type: 'POST',
  visibility: 'F',
  content: { text: 'Just for my followers' }
})

See Also