Actions API

Actions API

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

Overview

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

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
REACT React to content None (broadcast) Likes, loves
FLLW Follow a user/community Target user Subscribe to updates
CONN Connection request Target user Friend requests
MSG Private message Specific user Direct messages
FSHR File sharing Specific user(s) Share documents
ACK Acknowledgment Parent action issuer Read receipts
REPOST Share existing content Followers Retweets
SHRE Share resource/link Followers, Custom Link sharing
RSTAT Reaction statistics System Aggregated stats

Endpoints

List Actions

GET /api/actions

Query all actions with optional filters.

Query Parameters:

Filtering:

  • type - Filter by action type (POST, CMNT, REACT, FLLW, CONN, MSG, FSHR, etc.)
  • status - Filter by status (P=Pending, A=Active, R=Rejected, C=Cancelled, N=Neutral)
  • issuerTag - Filter by issuer identity (e.g., alice@example.com)
  • issuerTnId - Filter by issuer tenant ID (numeric)
  • audienceTag - Filter by audience identity
  • audienceTnId - Filter by audience tenant ID
  • 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)
  • attachments - Filter by file attachments (comma-separated file IDs)

Time-based:

  • createdAfter - Unix timestamp in seconds (e.g., 1735000000)
  • createdBefore - Unix timestamp in seconds
  • updatedAfter - Unix timestamp in seconds
  • updatedBefore - Unix timestamp in seconds

Pagination:

  • _limit - Max results (default: 50, max: 200)
  • _offset - Skip N results (for pagination)

Expansion:

  • _expand - Expand related objects (comma-separated)
    • issuer - Include full issuer profile
    • audience - Include full audience profile
    • subject - Include full subject profile
    • parent - Include parent action
    • attachments - Include file descriptors

Sorting:

  • _sort - Sort field (e.g., createdAt, updatedAt)
  • _order - Sort order (asc or desc, default: desc)

Examples:

Get recent posts with full issuer profiles:

const api = cloudillo.createApiClient()

const posts = await api.actions.get({
  type: 'POST',
  status: 'A',
  _limit: 20,
  _expand: 'issuer',
  _sort: 'createdAt',
  _order: 'desc'
})

Get comments on a specific post:

const comments = await api.actions.get({
  type: 'CMNT',
  parentId: 'act_post123',
  _expand: 'issuer',
  _sort: 'createdAt',
  _order: 'asc'
})

Get all actions involving a specific user:

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

Get posts from a time range:

const lastWeek = Math.floor(Date.now() / 1000) - 7 * 24 * 60 * 60

const recentPosts = await api.actions.get({
  type: 'POST',
  status: 'A',
  createdAfter: lastWeek,
  _expand: 'issuer,attachments',
  _limit: 100
})

Get pending connection requests:

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

Get thread with all nested comments:

const thread = await api.actions.get({
  rootId: 'act_original_post',
  type: 'CMNT',
  _sort: 'createdAt',
  _order: 'asc',
  _expand: 'issuer',
  _limit: 200
})

Response:

{
  "data": [
    {
      "actionId": "act_abc123",
      "type": "POST",
      "issuerTag": "alice@example.com",
      "issuer": {
        "idTag": "alice@example.com",
        "name": "Alice Johnson",
        "profilePic": "/file/b1~abc"
      },
      "content": {
        "text": "Hello, Cloudillo!",
        "title": "My First Post"
      },
      "createdAt": 1735000000,
      "stat": {
        "reactions": 5,
        "comments": 3,
        "ownReaction": "LOVE"
      }
    }
  ],
  "pagination": {
    "total": 150,
    "offset": 0,
    "limit": 20,
    "hasMore": true
  }
}

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.post({
  type: 'POST',
  content: {
    text: 'Hello, Cloudillo!',
    title: 'My First Post'
  },
  attachments: ['file_123', 'file_456']
})

Create a Comment:

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

Create a Reaction:

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

Follow a User:

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

Connect with a User:

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

Share a File:

const share = await api.actions.post({
  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.id('act_123').get()

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 Action

PATCH /api/actions/:actionId

Update an action. Only draft actions (status: ‘P’) can be updated, and only by the issuer.

Authentication: Required (must be issuer)

⚠️ Implementation Note: This endpoint may not be fully implemented and could return an error. Verify with the server implementation before using in production. Consider creating a new action instead of updating if this endpoint fails.

Request Body:

{
  content?: unknown // Updated content
  attachments?: string[] // Updated file list
  status?: string // Change status (e.g., 'P' to 'A' to publish)
}

Example:

const updated = await api.actions.id('act_123').patch({
  content: {
    text: 'Updated text',
    title: 'Updated Title'
  }
})

// Publish a draft
await api.actions.id('act_draft').patch({
  status: 'A'
})

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.id('act_123').delete()

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)

⚠️ Implementation Note: This endpoint is currently a stub and may not fully implement acceptance logic. It logs the action but may not update the action status or trigger side effects. Check with the server implementation before relying on this endpoint in production.

Example:

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

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

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)

⚠️ Implementation Note: This endpoint is currently a stub and may not fully implement rejection logic. It logs the action but may not update the action status or trigger side effects. Check with the server implementation before relying on this endpoint in production.

Example:

await api.actions.id('act_connreq123').reject.post()

Response:

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

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.id('act_post123').reaction.post({
  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.

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": 1735000000,
  "reqId": "req_abc123"
}

Note: This endpoint verifies the action signature against the issuer’s public key before accepting it.

Action Status Flow

Actions have a lifecycle represented by status:

P (Pending) → A (Active/Accepted)
            ↘ R (Rejected)
            ↘ C (Cancelled)
  • P (Pending): Draft or awaiting acceptance
  • A (Active): Published and visible
  • R (Rejected): Declined by recipient
  • C (Cancelled): Cancelled by issuer
  • N (Neutral): No specific status

Status transitions:

  • Only Pending actions can be accepted/rejected
  • Only Pending actions can be updated
  • Any action can be deleted by issuer
  • Accepting changes status to Active
  • Rejecting changes status to Rejected

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.action.id('act_123').get()

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.post({
  type: 'POST',
  content: { text: 'Main post' }
})

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

// Reply to comment
const reply = await api.actions.post({
  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.get({
  rootId: 'act_post123',
  type: 'CMNT',
  _sort: 'createdAt',
  _order: '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. Always Expand Relations

// ❌ Don't fetch actions and profiles separately
const actions = await api.actions.get({ type: 'POST' })
for (const action of actions.data) {
  const issuer = await api.profiles.get({ idTag: action.issuerTag })
}

// ✅ Use _expand to get related data in one request
const actions = await api.actions.get({
  type: 'POST',
  _expand: 'issuer,audience'
})

2. Use Pagination

// ✅ Paginate large result sets
let offset = 0
const limit = 50

while (true) {
  const result = await api.actions.get({
    type: 'POST',
    _limit: limit,
    _offset: offset
  })

  // Process result.data

  if (!result.pagination.hasMore) break
  offset += limit
}

3. Handle Drafts

// Create as draft, then publish
const draft = await api.actions.post({
  type: 'POST',
  status: 'P', // Draft
  content: { text: 'Work in progress...' }
})

// Edit draft
await api.actions.id(draft.actionId).patch({
  content: { text: 'Final version!' }
})

// Publish (change status to Active)
await api.actions.id(draft.actionId).patch({
  status: 'A'
})

4. Optimistic UI Updates

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

setPosts([optimisticAction, ...posts])

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

See Also