Wasim's Site
Back to all articles
Guides

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, ContactCompany — 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.


Wasim Shaikh

About the Author

Wasim Shaikh is an experienced UI/UX Developer & Front-End Engineer with 15+ years of expertise. Based in Ahmedabad, Gujarat, India, he specializes in Liferay, React, Angular, Next.js, Tailwind CSS, and CMS integrations. He regularly shares insights on web development, SEO, and performance optimization through his blog wasimshaikh.com.