Since v8.0 XavaPro automatically generates and publishes a REST API for all your JPA entities. You do not need to write controllers, DTOs, mappers, or configuration files: just write your domain entities with standard JPA and OpenXava annotations, and the REST API is immediately available.
The generated API includes:
/api/customers./api/.@Tab(baseCondition=...), History, email
notifications, and multitenancy.By default, the automatic API is turned off for security. You can easily activate it from the administration user interface without restarting the server.
Go to the Admin folder in your application menu and open the Configuration module. In the configuration form, check the Auto generate API checkbox and click Save:

Once checked, all endpoints under the /api/ path become active
immediately. If unchecked, any incoming request to /api/* receives
an HTTP 404 response with a JSON body {"error":"API is disabled"}.
XavaPro includes an embedded, offline Swagger UI explorer that allows you to inspect and interactively test every available endpoint directly from your browser.
To access Swagger UI, open the following URL in your browser:
http://localhost:8080/yourapp/api/

From the Swagger UI interface, you can click the Authorize
button, paste the JWT (without the Bearer prefix; Swagger UI
adds it), explore the JSON schemas of your entities,
and execute queries directly against your application.
The raw OpenAPI 3.0 specification in JSON format is available at:
GET /api/openapi.json
Security note: /api/ and
/api/openapi.json are public: they do not require a JWT and
they describe every module that maps to an entity. Treat that as part of
your attack surface in production (restrict network access, or keep the
API disabled until you need it).
All REST API endpoints (except /api/login, /api/,
and /api/openapi.json) are protected and require a JSON Web
Token (JWT) passed in the Authorization HTTP header.
To authenticate, send a POST request to /api/login
with the XavaPro user credentials:
curl -X POST http://localhost:8080/yourapp/api/login \
-H "Content-Type: application/json" \
-d '{"user": "admin", "password": "adminpassword"}'
If the credentials are valid and the user account is active, the server returns an HTTP 200 response with the token:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
The token is valid for 24 hours. After it expires,
/api/login must be called again. Include the token in all
subsequent requests using the Bearer scheme:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Tokens are cryptographically signed using the HMAC-SHA256 algorithm.
For production environments, configure the secret key by setting
the XAVA_JWT_SECRET environment variable on your server:
export XAVA_JWT_SECRET="YourSuperSecretKeyMustBeAtLeast32CharactersLong!"
Note: If the XAVA_JWT_SECRET variable is not set or
has fewer than 32 characters, OpenXava generates an ephemeral random secret
key on application startup. In that case, existing tokens will be invalidated
whenever the server restarts.
The automatic API strictly enforces the same security model defined in XavaPro for the web interface:
GET requests are permitted.
POST, PUT, and DELETE return HTTP 403.POST)
requires permission for the new action; updating records (PUT)
requires permission for the save action; deleting records (DELETE)
requires permission for the delete action.GET responses and ignored
on POST and PUT.GET responses but ignored
during PUT modifications.You can create a dedicated api role (or multiple API roles) in the Roles module specifically for API consumers. Assigning this role to the API user accounts lets you fine-tune exactly which modules, actions, and fields are accessible through the REST API.
| HTTP Method | Endpoint | Description | Success Status |
|---|---|---|---|
POST |
/api/login |
Authenticate user and obtain JWT token | 200 OK |
GET |
/api/openapi.json |
OpenAPI 3.0 specification (public) | 200 OK |
GET |
/api/ |
Interactive Swagger UI explorer (public) | 200 OK |
GET |
/api/{entity} |
List records (with pagination, sorting, and filtering) | 200 OK |
GET |
/api/{entity}/{id} |
Get a single record by primary key | 200 OK |
POST |
/api/{entity} |
Create a new record | 201 Created |
PUT |
/api/{entity}/{id} |
Update the fields sent in the body (partial update) | 200 OK |
DELETE |
/api/{entity}/{id} |
Delete a record | 204 No Content |
The path segment is the entity name in lower case, pluralized with
simple English heuristics: Customer becomes
/api/customers, Country becomes
/api/countries, Invoice becomes
/api/invoices. Names that already end in
s (except ss) are left as-is after lowercasing.
If a generated URL surprises you, check Swagger UI or
/api/openapi.json for the exact path.
Sends a GET request to the plural entity URL:
curl -X GET "http://localhost:8080/yourapp/api/customers" \
-H "Authorization: Bearer $TOKEN"
Response:
[
{
"number": 1,
"name": "ACME Corporation",
"creditLimit": 15000.00,
"address": {
"street": "Main Street 123",
"city": "Valencia",
"zipCode": 46001
}
},
{
"number": 2,
"name": "Global Tech Ltd",
"creditLimit": 25000.00,
"address": {
"street": "Market Square 4",
"city": "Madrid",
"zipCode": 28001
}
}
]
Sends a GET request with the primary key in the path:
curl -X GET "http://localhost:8080/yourapp/api/customers/1" \
-H "Authorization: Bearer $TOKEN"
For entities with a composite primary key, separate the key values
with commas in the order of property declaration. For an entity whose
key properties are year and number:
curl -X GET "http://localhost:8080/yourapp/api/warehouseorders/2026,45" \
-H "Authorization: Bearer $TOKEN"
Use the real key of the entity. An Invoice that is
identified by a UUID oid is retrieved as
/api/invoices/{oid}, not as year,number.
Sends a POST request with the entity data as a JSON payload
in the request body:
curl -X POST "http://localhost:8080/yourapp/api/customers" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"number": 105,
"name": "Innovate Software Inc",
"creditLimit": 50000.00,
"address": {
"street": "Tech Park Ave 12",
"city": "Barcelona",
"zipCode": 8001
}
}'
The server returns HTTP 201 Created and the complete JSON representation of the created entity.
To assign a @ManyToOne reference, send only its
primary-key fields. For an invoice whose customer is identified by
number:
curl -X POST "http://localhost:8080/yourapp/api/invoices" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"year": 2026,
"number": 1,
"customer": { "number": 1 }
}'
Sends a PUT request specifying the primary key in the URL
and the fields to change in the JSON body. Only the properties present
in the body are updated; omitted fields keep their current values
(a partial update, not a full replace):
curl -X PUT "http://localhost:8080/yourapp/api/customers/105" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Innovate Software Group",
"creditLimit": 75000.00
}'
The server returns HTTP 200 OK along with the updated entity.
Sends a DELETE request with the entity ID:
curl -X DELETE "http://localhost:8080/yourapp/api/customers/105" \
-H "Authorization: Bearer $TOKEN"
Upon successful deletion, the server responds with HTTP 204 No Content.
All list endpoints support pagination through two query parameters:
page: The 1-based page index (defaults to 1).size: The number of items per page (defaults to 20,
maximum 100).curl -X GET "http://localhost:8080/yourapp/api/customers?page=2&size=10" \
-H "Authorization: Bearer $TOKEN"
Pagination metadata is communicated through HTTP response headers:
X-Total-Count: The total number of records matching the query.Link: Standard RFC 5988 header providing navigational links
to rel="first", rel="prev", rel="next",
and rel="last". Filter and sort parameters are
preserved in these links.Example response headers:
X-Total-Count: 45
Link: <http://localhost:8080/yourapp/api/customers?page=1&size=10>; rel="first", <http://localhost:8080/yourapp/api/customers?page=1&size=10>; rel="prev", <http://localhost:8080/yourapp/api/customers?page=3&size=10>; rel="next", <http://localhost:8080/yourapp/api/customers?page=5&size=10>; rel="last"
Use the sort query parameter to order query results by one
or more properties:
sort=name).sort=-number).sort=address.city,-creditLimit).sort=seller.name).curl -X GET "http://localhost:8080/yourapp/api/customers?sort=address.city,-name" \
-H "Authorization: Bearer $TOKEN"
An unknown sort field returns HTTP 400.
Query results can be filtered by specifying query parameters that combine
the property name with an explicit operator suffix:
{field}_{operator}={value}.
| Operator | Description | Example |
|---|---|---|
_eq |
Equal to | number_eq=100 |
_ne |
Not equal to | type_ne=PROSPECT |
_gt |
Greater than | creditLimit_gt=5000 |
_gte |
Greater than or equal to | creditLimit_gte=5000 |
_lt |
Less than | creditLimit_lt=10000 |
_lte |
Less than or equal to | creditLimit_lte=10000 |
_like |
Pattern matching on strings (* for wildcard, ? for single character) |
name_like=ACME* |
_in |
In a comma-separated list of values | number_in=1,5,22 |
_null |
Check if null (true for IS NULL, false for IS NOT NULL) |
remarks_null=true |
Filters can be combined (they are evaluated with logical AND)
and can traverse relationships using dotted paths:
curl -X GET "http://localhost:8080/yourapp/api/customers?address.city_like=Val*&creditLimit_gte=10000&sort=-creditLimit" \
-H "Authorization: Bearer $TOKEN"
The automatic API serializes references and embedded collections naturally into JSON:
@Embedded or @ManyToOne are serialized as nested JSON
objects containing their properties and primary key.@ElementCollection are automatically serialized as nested JSON
arrays of objects and can be created or updated directly within the parent payload.Operations performed through the REST API seamlessly trigger existing OpenXava business behaviors:
XavaPro's multi-organization architecture is fully supported by the automatic REST API. Each organization has its own isolated API namespace:
| Resource | URL Pattern |
|---|---|
| Organization Login | POST /api/orgs/{organization}/login |
| Organization Swagger UI | GET /api/orgs/{organization}/ |
| Organization OpenAPI Spec | GET /api/orgs/{organization}/openapi.json |
| Organization Entity CRUD | /api/orgs/{organization}/{entity} |
Authenticate against the organization and then call that same
namespace. The JWT does not carry the organization: the tenant is
selected by the URL. Use a token obtained from
/api/orgs/{organization}/login against that organization.
Example request to query customers belonging to organization acme_corp:
curl -X GET "http://localhost:8080/yourapp/api/orgs/acme_corp/customers" \
-H "Authorization: Bearer $TOKEN"
To enable web applications hosted on other domains (such as a separate React, Angular, or Vue frontend) to call your API, you can configure Cross-Origin Resource Sharing (CORS).
In the Admin > Configuration module, set the Api allowed origins property:

You can specify:
*) to permit requests from any origin.
Credentials are not allowed when the wildcard is used.http://localhost:3000, https://app.example.com).
In this case the matching origin is echoed back and
Access-Control-Allow-Credentials: true is sent.The server automatically handles OPTIONS preflight requests
and adds the necessary CORS response headers (Access-Control-Allow-Origin,
Access-Control-Allow-Methods, Access-Control-Allow-Headers,
and Access-Control-Expose-Headers for X-Total-Count
and Link).
XAVA_JWT_SECRET to a stable secret of at least 32
characters so tokens survive server restarts./api/openapi.json are
public and expose the model.* if those
clients send cookies or other credentials./api/orgs/{organization}/... and obtain the token from
that organization's login.Here is a complete JavaScript example using fetch to authenticate,
fetch a paginated list of customers with filtering, and create a new record:
const API_BASE = 'http://localhost:8080/yourapp/api';
async function main() {
// 1. Authenticate and obtain JWT
const loginRes = await fetch(`${API_BASE}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user: 'admin', password: 'adminpassword' })
});
const { token } = await loginRes.json();
// 2. Query customers with filtering and pagination
const listRes = await fetch(
`${API_BASE}/customers?address.city_like=Val*&creditLimit_gte=5000&sort=-creditLimit&page=1&size=10`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);
const totalCount = listRes.headers.get('X-Total-Count');
const customers = await listRes.json();
console.log(`Found ${totalCount} customers:`, customers);
// 3. Create a new customer
const createRes = await fetch(`${API_BASE}/customers`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
number: 201,
name: 'New Horizon SL',
creditLimit: 30000,
address: {
street: 'Avenida del Puerto 45',
city: 'Valencia',
zipCode: 46021
}
})
});
const created = await createRes.json();
console.log('Customer created successfully:', created);
}
main().catch(console.error);
@Tab and baseCondition)