REST APIs Auto-Generated by Liferay Objects: What You Get Out of the Box
Published on July 8, 2026 by Wasim
Overview
Publishing a Liferay Object doesn't just create a data model — it also generates a complete REST API (and matching GraphQL schema) with full CRUD, filtering, sorting, pagination, and relationship support. This is a practical reference for what those APIs look like and how to use them, based on Liferay's official Custom Object APIs documentation.
Finding Your Object's API
Every Object-generated API is discoverable through Liferay's API Explorer at:
http://your-liferay-instance:8080/o/api
Look under REST Applications for entries prefixed with c/ — that prefix marks custom Objects. The Object API Basics guide covers the full endpoint structure, but the pattern is predictable:
Base path: /o/c/{object-name-plural}
Object "Contact" → /o/c/contacts
Object "DistributorApp" → /o/c/distributorapps
Object "KYCVerification" → /o/c/kycverifications
Liferay lowercases and hyphenates the Object's plural label automatically to build the path.
The CRUD Endpoints
Every Object gets six endpoints covering create, read, update, and delete:
| Method | Endpoint | Purpose |
|---|---|---|
GET |
/o/c/{objects} |
List records (paginated) |
GET |
/o/c/{objects}/{id} |
Retrieve one record |
POST |
/o/c/{objects} |
Create a record |
PUT |
/o/c/{objects}/{id} |
Replace a record entirely |
PATCH |
/o/c/{objects}/{id} |
Update only the fields you send |
DELETE |
/o/c/{objects}/{id} |
Remove a record |
Example — creating a contact:
curl -X POST 'http://localhost:8080/o/c/contacts' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"name": "Acme Corporation", "email": "contact@acme.com", "status": "Active"}'
{
"id": 12347,
"name": "Acme Corporation",
"email": "contact@acme.com",
"status": "Active",
"dateCreated": "2026-07-08T10:00:00Z"
}
PUT vs. PATCH is the detail worth remembering: PUT replaces the entire record, so any field you omit gets reset to its default. PATCH only touches the fields you include — send {"status": "Inactive"} via PATCH and every other field stays exactly as it was. Default to PATCH unless you deliberately want a full overwrite; it's the safer choice for partial updates from forms or automations.
A successful POST returns 201 Created, a successful DELETE returns 204 No Content with an empty body, and validation failures return 400 Bad Request with a fieldErrors array naming the offending fields.
Filtering, Sorting, and Pagination
Liferay's headless APIs support a subset of the OData standard for filtering, documented under API Query Parameters. The three parameters you'll use constantly:
?filter=status eq 'Active'
?sort=dateCreated:desc
?page=2&pageSize=50
Common filter operators: eq, ne, lt, gt, startsWith(field,'value'), contains(field,'value'), combined with and / or. They compose:
GET /o/c/contacts?filter=status eq 'Active' and contains(name,'Corp')&sort=name:asc&pageSize=20
Reduce payload size further with field selection — ?fields=id,name,email returns only those three fields instead of the full record, which matters once an Object accumulates a lot of columns or you're calling the API from a mobile client.
Relationships
If an Object has a relationship field — say, Contact → Company — the default response only includes the related record's ID:
{ "id": 12345, "name": "John Smith", "company": 100 }
To pull the full related record in the same request, add nestedFields:
GET /o/c/contacts/12345?nestedFields=company
{
"id": 12345,
"name": "John Smith",
"company": { "id": 100, "name": "Acme Corp", "industry": "Technology" }
}
This avoids a second round-trip whenever a UI or integration needs both records together.
Authentication
Production integrations should use OAuth 2.0, not basic auth. Liferay's OAuth 2.0 documentation covers three flows — authorization code, password, and client credentials. For server-to-server integration (no end user involved), create an OAuth2 application with Client Profile: Headless Server and the Client Credentials authorization type enabled, then request a token:
curl -X POST 'http://localhost:8080/o/oauth2/token' \
-d 'grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET'
Pass the returned token as a bearer token on every subsequent call:
curl -H 'Authorization: Bearer eyJhbGciOi...' 'http://localhost:8080/o/c/contacts'
See Using OAuth2 to Authorize Users for the full setup, including scoping tokens to specific permissions.
Error Handling
| Status | Meaning |
|---|---|
200 |
Successful GET / PUT / PATCH |
201 |
Record created |
204 |
Record deleted, no content returned |
400 |
Invalid input — check fieldErrors |
401 |
Missing or expired token |
403 |
Authenticated, but no permission |
404 |
Object or record doesn't exist |
409 |
Conflict — usually a duplicate value on a unique field |
Error bodies follow a consistent shape, which makes generic retry/error-handling logic easy to write once and reuse across every Object:
{
"status": 400,
"title": "Bad Request",
"detail": "Contact with email already exists",
"fieldErrors": [{ "field": "email", "message": "This email is already registered" }]
}
Bulk Operations
For large-scale import/export rather than one-record-at-a-time calls, Liferay provides a dedicated Batch Engine API rather than a generic /batch route on each Object. It runs import/export as asynchronous tasks (status: INITIAL, COMPLETED, or FAILED) and supports CSV, JSON, and XLSX. For custom Objects specifically, see Using Batch APIs — it explains how to target an Object's ObjectEntry class name for the import/export task. This is the right tool once you're moving hundreds or thousands of records at once, rather than looping individual POST calls.
Putting It Together: The KYC Example, End to End
In the Objects overview article we modeled a DistributorApplication Object linked to a KYCVerification Object. Here's the same workflow driven entirely through the REST API:
# 1. Create the application
curl -X POST '.../o/c/distributorapplications' -d '{"businessName":"TechCorp LLC","status":"Pending"}'
# 2. Query pending applications, newest first
curl '.../o/c/distributorapplications?filter=status eq '\''Pending'\''&sort=dateCreated:desc'
# 3. Update status after the KYC check runs
curl -X PATCH '.../o/c/distributorapplications/5001' -d '{"status":"Approved"}'
# 4. Create the linked verification record
curl -X POST '.../o/c/kycverifications' -d '{"application":5001,"riskLevel":"Low"}'
# 5. Fetch the application with its verification nested in one call
curl '.../o/c/distributorapplications/5001?nestedFields=kycVerification'
Five HTTP calls, zero custom backend code — everything from the data model to the query layer came from publishing two Objects.
Final Thoughts
The practical implication of Object-generated APIs is that the "backend work" for a new business process often disappears entirely. You still need to think about relationships, permissions, and query patterns — but the endpoints, validation, and error handling are handled for you the moment you publish. Combine that with OAuth 2.0 for security and the Batch Engine for bulk data, and Objects cover the same ground a hand-built CRUD API would, without maintaining any of the API code yourself.
Related Reading
- Liferay Objects Explained: Building Custom Data Models Without Code — how to create the Objects these APIs are generated from.
- Creating Your First Liferay Object: A Step-by-Step Tutorial — build a Contact Object from scratch and call the exact API shown in Step 8.
- Liferay Low-Code: Build Business Apps Without the Hand-Coding — where Objects fit alongside Workflows and interface design.
- Browse all Liferay tools — generators and reference utilities for Liferay developers.

