Define an entity class
This guide shows you how to create an entity class with property definitions on your keyset using the Admin API. Property definitions control write-time validation, filtering, and field-level access for every entity you create against that class.
Before you start
- DataSync must be enabled on your keyset. Refer to Enable DataSync.
- You need an Admin API key. Create a Service Integration with a permission row for the DataSync resource, at Keyset level for your keyset, with Read & write access. Copy the API key when you create it, because the Admin Portal shows it only once.
- Keep the API key on your server. It manages class definitions only. Your clients create and read entities with Access Manager tokens instead, as described in DataSync access control.
Create an entity class
Call Create a new entity class. The class name and version go in the path, and the class definition goes in the data object of the body.
1curl -X POST 'https://admin-api.pubnub.com/v2/datasync/subkeys/{subKey}/entity-classes/Product/versions/1' \
2 -H 'Authorization: {apiKey}' \
3 -H 'PubNub-Version: 2026-09-17' \
4 -H 'Content-Type: application/vnd.pubnub.objects.entity-class+json;version=1' \
5 -d '{
6 "data": {
7 "config": {
8 "ttlSec": 2678400
9 },
10 "properties": [
11 {
12 "name": "name",
13 "path": "/payload/name",
14 "valueKind": "string",
15 "filtering": "full",
show all 37 lines| Part | Value |
|---|---|
{subKey} | The subscribe key of the keyset. |
Product, 1 | The class name and the class version, in the path. |
Authorization | The Service Integration API key. |
PubNub-Version | The Admin API version. A request without this header is rejected with a 428. |
Content-Type | Must be application/vnd.pubnub.objects.entity-class+json;version=1. application/json is rejected with a 415. |
config | Required for a class that doesn't extend another class. Send {} to use the default TTL. |
A successful request returns 201 Created with the stored class:
{
"data": {
"level": "SubKey",
"createdAt": "2026-09-23T21:35:15.747618137Z",
"updatedAt": "2026-09-23T21:35:15.747618137Z",
"eTag": "3w5e111sv9tng",
"version": 1,
"name": "Product",
"config": {"ttlSec": 2678400},
"properties": [
{
"name": "name",
"path": "/payload/name",
"valueKind": "string",
"filtering": "full",
show all 37 lineslevel is SubKey for every class you define on a keyset. The built-in User and Channel classes have level set to Global.
A price property declared as filtering: simple validates every write and makes price filterable with filter_fast. A name property declared as filtering: full is available for both filter_fast and eventually consistent filter queries.
The projections list on each property controls who sees it. A client reading through the public projection gets name and price but not stock. Refer to Projections.
If the request fails, the response lists the reasons in errors. The most common ones:
| Status | Error code | Cause |
|---|---|---|
400 | DS-0006 | config is missing on a class that doesn't extend another class. |
400 | DS-0004 | A body field failed validation, for example a body not wrapped in data. The path field names the field. |
409 | DS-0301 | This class name and version already exist on the keyset. |
415 | DS-0007 | The Content-Type isn't supported. |
For every code, refer to DataSync error codes.
Choosing a filtering mode
| Mode | Use when |
|---|---|
none | Validation only; you'll never filter on this field |
simple | You need strongly consistent filtering (filter_fast) |
full | You need both filtering tiers, or eventually consistent search |
If you plan to combine a property with filter (eventually consistent) in a sort expression, declare it as full. A simple property sorts correctly under filter_fast but is rejected when combined with filter.
Set a class TTL
Every entity class has a config.ttlSec setting. Send config without ttlSec, as {}, and DataSync uses the default of 2,678,400 seconds (31 days). To keep entities longer, for example long-lived user profiles, set a higher value. The maximum is 315,569,260 seconds (approximately 10 years).
1curl -X POST 'https://admin-api.pubnub.com/v2/datasync/subkeys/{subKey}/entity-classes/UserProfile/versions/1' \
2 -H 'Authorization: {apiKey}' \
3 -H 'PubNub-Version: 2026-09-17' \
4 -H 'Content-Type: application/vnd.pubnub.objects.entity-class+json;version=1' \
5 -d '{
6 "data": {
7 "config": {
8 "ttlSec": 31536000
9 }
10 }
11 }'
Extend a Global class
To add fields to users or channels, define a class that extends the Global User or Channel class, rather than trying to modify the Global class itself. The extends reference takes the parent's name and version. PubNub seeds User and Channel at version 1.
1curl -X POST 'https://admin-api.pubnub.com/v2/datasync/subkeys/{subKey}/entity-classes/MarketplaceUser/versions/1' \
2 -H 'Authorization: {apiKey}' \
3 -H 'PubNub-Version: 2026-09-17' \
4 -H 'Content-Type: application/vnd.pubnub.objects.entity-class+json;version=1' \
5 -d '{
6 "data": {
7 "extends": {
8 "name": "User",
9 "version": 1
10 },
11 "properties": [
12 {
13 "name": "display_name",
14 "path": "/payload/display_name",
15 "valueKind": "string",
show all 30 linesThe subclass inherits the name and type properties from the Global User class automatically, so you only declare the fields you're adding. Without its own config, it also inherits User's TTL of 2,592,000 seconds (30 days).
email belongs only to the admin projection. A client that reads a MarketplaceUser without the admin projection doesn't get email.
Define a relationship class
To link two entities with your own relationship type, define a relationship class. This Wishlist class links a MarketplaceUser to a Product. Relationship classes use their own endpoint and content type.
1curl -X POST 'https://admin-api.pubnub.com/v2/datasync/subkeys/{subKey}/relationship-classes/Wishlist/versions/1' \
2 -H 'Authorization: {apiKey}' \
3 -H 'PubNub-Version: 2026-09-17' \
4 -H 'Content-Type: application/vnd.pubnub.objects.relationship-class+json;version=1' \
5 -d '{
6 "data": {
7 "cardinality": "many-to-many",
8 "entityAClass": "MarketplaceUser",
9 "entityBClass": "Product"
10 }
11 }'
cardinality is one-to-one, one-to-many, or many-to-many. entityAClass and entityBClass restrict which entity class each side accepts. Refer to Create a new relationship class.
Add a new class version
To evolve a class without breaking existing instances, add a new version. Existing instances keep the validation and filtering behavior of the version they were created against.
Call Create a new entity class with the same class name and a new integer version in the path, for example /entity-classes/Product/versions/2.
Updating a version replaces all property definitions
Updating a class version replaces its entire property-definition set. Partial class updates aren't implemented. Send every property you want the version to keep, including inherited ones you want to redeclare.