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/actionsQuery 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 identityparentId- 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 IDtag- 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:ascordesc(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/actionsCreate 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/:actionIdRetrieve 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/:actionIdDelete 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/acceptAccept 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/rejectReject 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/statUpdate action statistics (typically called by the system).
Authentication: Required (admin)
Request Body:
{
reactions?: number
comments?: number
views?: number
}Add Reaction
POST /api/actions/:actionId/reactionAdd 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/inboxReceive 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/syncReceive 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
- Authentication - How to authenticate requests
- Files API - Attach files to actions
- Profiles API - User and community profiles
- WebSocket API - Real-time action updates