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
    }
  }
}

Update Draft Action

PATCH /api/actions/{action_id}

Update a draft action before publishing. Only actions in draft status (R) can be updated.

Authentication: Required (must be issuer)

Request Body:

{
  "content": {
    "text": "Updated post content",
    "title": "Updated Title"
  },
  "attachments": ["b1~file123"],
  "visibility": "P"
}
Field Type Description
content object Updated content (action-type specific)
attachments string[] Updated file attachment IDs
visibility string Visibility level (P, V, F, C)
flags string Action flags
x object Extension data

Response:

{
  "data": {
    "actionId": "act_123",
    "type": "POST",
    "status": "R",
    "content": {
      "text": "Updated post content",
      "title": "Updated Title"
    }
  },
  "time": "2025-01-01T12:00:00Z"
}
Info

Only draft actions (status R) can be updated. Published actions are signed JWTs and cannot be modified.

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
}

Dismiss Notification

POST /api/actions/{action_id}/dismiss

Dismiss a notification action. This acknowledges a notification without accepting or rejecting it.

Authentication: Required

Request Body: Empty

Response:

{
  "data": null,
  "time": "2025-01-01T12:00:00Z"
}

Publish Draft

POST /api/actions/{action_id}/publish

Publish a draft action, making it visible to the intended audience. Optionally schedule for future publication.

Authentication: Required (must be issuer)

Request Body:

{
  "publishAt": "2025-02-01T12:00:00Z"
}
Field Type Description
publishAt string Optional future publication timestamp (ISO 8601). If omitted, publishes immediately.

Response:

{
  "data": {
    "actionId": "act_123",
    "type": "POST",
    "status": "A"
  },
  "time": "2025-01-01T12:00:00Z"
}
Info

If publishAt is provided, the action moves to scheduled status (S) and will be published automatically at the specified time.

Cancel Scheduled Action

POST /api/actions/{action_id}/cancel

Cancel a scheduled action, reverting it back to draft status.

Authentication: Required (must be issuer)

Request Body: Empty

Response:

{
  "data": {
    "actionId": "act_123",
    "type": "POST",
    "status": "R"
  },
  "time": "2025-01-01T12:00:00Z"
}

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:

R (Draft)     → S (Scheduled) → A (Active/Published)
              → A (Active)      ↘ R (Cancelled)
              ↘ D (Deleted)

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

N (New)       → (Dismissed)

P (Pending)   → A (Active)
              ↘ D (Deleted)
  • R (Draft): Unpublished draft, can be edited
  • S (Scheduled): Scheduled for future publication
  • 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
  • P (Pending): Legacy pending status

Status transitions:

  • Draft actions can be updated, published, scheduled, or deleted
  • Scheduled actions can be cancelled (reverts to Draft) or auto-publish at scheduled time
  • Created actions can be accepted (→ Active) or rejected (→ Deleted)
  • New notifications can be dismissed
  • Any action can be deleted by issuer (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