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/actionsQuery 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 identityaudienceTnId- Filter by audience tenant IDparentId- 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 secondsupdatedAfter- Unix timestamp in secondsupdatedBefore- 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 profileaudience- Include full audience profilesubject- Include full subject profileparent- Include parent actionattachments- Include file descriptors
Sorting:
_sort- Sort field (e.g.,createdAt,updatedAt)_order- Sort order (ascordesc, 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/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.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/:actionIdRetrieve 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/:actionIdUpdate 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/:actionIdDelete 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/acceptAccept 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/rejectReject 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/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.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/inboxReceive 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
- Authentication - How to authenticate requests
- Files API - Attach files to actions
- Profiles API - User and community profiles
- WebSocket API - Real-time action updates