Content CRUD Examples
This guide demonstrates how to perform Create, Read, Update, and Delete operations on content (contentlets) in dotCMS using the REST API. This is a quick how-to sketch; for a more complete reference on content lifecycle actions via API, see Saving Content via API.
All write operations go through the same Workflow API endpoint — only the action name in the path changes:
PUT /api/v1/workflow/actions/default/fire/{ACTION}
The POST equivalent of the same endpoint operates over multiple contentlets at once:
Get an API Token#
Before making any API calls, get a token and store it in an environment variable:
export TOK=`curl -H "Content-Type:application/json" -s -X POST -d '{ "user": "[email protected]", "password": "admin", "expirationDays": 1, "label": "for testing" }' http://localhost:8082/api/v1/authentication/api-token \ | python3 -c 'import json,sys; print(json.load(sys.stdin)["entity"]["token"])'`
Alternatively, generate a token in the admin panel and set it manually:
export TOK=your_token_here
The examples below all use $TOK for authentication and $conId to hold the content identifier returned by the create step.
Create Content#
To create new content, fire the NEW system action. Omit identifier from the contentlet body and dotCMS will create a new contentlet; include it to update an existing one.
Endpoint: PUT /api/v1/workflow/actions/default/fire/NEW
Parameters:
language(query, optional): Language ID (defaults to -1)indexPolicy(query, optional): DEFER, WAIT_FOR, or FORCE
Request Body:
{ "contentlet": { "contentType": "BlogPost", "title": "My New Blog Post", "body": "This is the content of my blog post", "tags": "dotCMS,tutorial" } }
Example — create and store the returned identifier:
export conId=$(curl -s -X PUT "http://localhost:8082/api/v1/workflow/actions/default/fire/NEW?language=1" \ -H "Authorization: Bearer $TOK" \ -H "Content-Type: application/json" \ -d '{ "contentlet": { "contentType": "BlogPost", "title": "Getting Started with dotCMS", "body": "Welcome to our comprehensive guide..." } }' | python3 -c 'import json,sys; print(json.load(sys.stdin)["entity"]["identifier"])')
Tip: Including
"identifier": "..."in thecontentletbody withNEWwill save changes to an existing contentlet instead of creating a new one. This is equivalent to using theEDITaction.
Read Content#
Read Single Contentlet#
Endpoint: GET /api/v1/content/{inodeOrIdentifier}
Parameters:
inodeOrIdentifier(path, required): The contentlet identifier or inodelanguage(query, optional): Language ID for localizationvariantName(query, optional): Variant name (defaults to "DEFAULT")depth(query, optional): Relationship depth (-1 for none, defaults to -1)
Example:
curl -s -H "Authorization: Bearer $TOK" \ "http://localhost:8082/api/v1/content/$conId?language=1"
Search Multiple Contentlets#
Endpoint: POST /api/v1/content/search
Request Body:
{ "contentType": "BlogPost", "query": "dotCMS tutorials", "offset": 0, "perPage": 20, "sortBy": "modDate", "sortOrder": "desc" }
Example:
curl -s -X POST "http://localhost:8082/api/v1/content/search" \ -H "Authorization: Bearer $TOK" \ -H "Content-Type: application/json" \ -d '{"contentType": "BlogPost", "query": "dotCMS", "perPage": 10}'
Update Content#
To update existing content, use the EDIT system action. Pass the identifier either as a query parameter or inside the contentlet body.
Endpoint: PUT /api/v1/workflow/actions/default/fire/EDIT
Parameters:
identifier(query, optional): Identifier of the contentletinode(query, optional): Inode of the contentletlanguage(query, optional): Language ID (defaults to -1)indexPolicy(query, optional): DEFER, WAIT_FOR, or FORCE
Request Body:
{ "contentlet": { "identifier": "d66309a7378bbad381fda3accd7b2e80", "title": "Updated Blog Post Title", "body": "Updated content here..." }, "comments": "Updated title and body content" }
Example:
curl -s -X PUT "http://localhost:8082/api/v1/workflow/actions/default/fire/EDIT?identifier=$conId&language=1" \ -H "Authorization: Bearer $TOK" \ -H "Content-Type: application/json" \ -d '{ "contentlet": { "title": "Updated: Getting Started with dotCMS", "body": "This guide has been updated with new information..." }, "comments": "Updated content with latest information" }'
Delete Content#
To delete content, fire the DELETE system action.
Endpoint: PUT /api/v1/workflow/actions/default/fire/DELETE
Parameters:
identifier(query, optional): Identifier of the contentletinode(query, optional): Inode of the contentletlanguage(query, optional): Language ID (defaults to -1)
Request Body:
{ "contentlet": { "identifier": "d66309a7378bbad381fda3accd7b2e80" }, "comments": "Removing outdated content" }
Example:
curl -s -X PUT "http://localhost:8082/api/v1/workflow/actions/default/fire/DELETE?identifier=$conId&language=1" \ -H "Authorization: Bearer $TOK" \ -H "Content-Type: application/json" \ -d '{"comments": "Content no longer needed"}'
Other Workflow System Actions#
The same endpoint handles the full content lifecycle:
| Action | Effect |
|---|---|
PUBLISH | Make content live |
UNPUBLISH | Remove content from live environment |
ARCHIVE | Soft-delete (reversible) |
UNARCHIVE | Restore from archive |
DESTROY | Permanently delete |
Example — Publish:
curl -s -X PUT "http://localhost:8082/api/v1/workflow/actions/default/fire/PUBLISH?identifier=$conId&language=1" \ -H "Authorization: Bearer $TOK" \ -H "Content-Type: application/json" \ -d '{"comments": "Publishing approved content"}'
Full Lifecycle Walkthrough#
This sequence creates a content object, publishes it, retrieves it, unpublishes it, archives it, and deletes it — all chained via $conId.
# 1. Get a token export TOK=`curl -H "Content-Type:application/json" -s -X POST -d '{ "user": "[email protected]", "password": "admin", "expirationDays": 1, "label": "for testing" }' http://localhost:8082/api/v1/authentication/api-token \ | python3 -c 'import json,sys; print(json.load(sys.stdin)["entity"]["token"])'` # 2. Create content, capture the identifier export conId=$(curl -s -X PUT "http://localhost:8082/api/v1/workflow/actions/default/fire/NEW?language=1" \ -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \ -d '{"actionName":"save","comments":"creating","contentlet":{"contentType":"myBlog","title":"My Post","languageId":"1"}}' \ | python3 -c 'import json,sys; print(json.load(sys.stdin)["entity"]["identifier"])') # 3. Read it back curl -s -H "Authorization: Bearer $TOK" "http://localhost:8082/api/v1/content/$conId" # 4. Update it curl -s -X PUT "http://localhost:8082/api/v1/workflow/actions/default/fire/EDIT?identifier=$conId" \ -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \ -d '{"contentlet":{"title":"My Updated Post"},"comments":"edited"}' # 5. Publish it curl -s -X PUT "http://localhost:8082/api/v1/workflow/actions/default/fire/PUBLISH?identifier=$conId" \ -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \ -d '{"comments":"publishing"}' # 6. Unpublish it curl -s -X PUT "http://localhost:8082/api/v1/workflow/actions/default/fire/UNPUBLISH?identifier=$conId" \ -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \ -d '{"comments":"unpublishing"}' # 7. Archive it curl -s -X PUT "http://localhost:8082/api/v1/workflow/actions/default/fire/ARCHIVE?identifier=$conId" \ -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \ -d '{"comments":"archiving"}' # 8. Delete it curl -s -X PUT "http://localhost:8082/api/v1/workflow/actions/default/fire/DELETE?identifier=$conId" \ -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \ -d '{"comments":"done"}'
Notes#
- Content API handles reads; Workflow API handles writes
NEWwithoutidentifier= create;NEWwithidentifier= update (same asEDIT)- Always specify
languagewhen working with multilingual content - Use
indexPolicy: DEFERin production to avoid blocking on index writes - Always include
commentsin workflow operations — they appear in the workflow history - Actions are permission-based; users can only fire actions their role allows