API Explorer

Dynamic-Entities

Dynamic Entities allow you to create custom data structures and their corresponding CRUD endpoints at runtime without writing code or restarting the OBP-API instance.

Overview:

Dynamic Entities enable you to define custom business objects (entities) with their fields, types, and validation rules via API calls. Once created, OBP automatically generates fully functional REST API endpoints for Create, Read, Update, and Delete operations.

Types of Dynamic Entities:

  1. System Level Dynamic Entities - Available across the entire OBP instance
  2. Bank Level Dynamic Entities - Scoped to a specific bank

Creating a Dynamic Entity:

POST /management/system-dynamic-entities
{
  "hasPersonalEntity": true,
  "CustomerPreferences": {
    "description": "Customer preferences and settings",
    "required": ["theme"],
    "properties": {
      "theme": {
        "type": "string",
        "example": "dark"
      },
      "language": {
        "type": "string",
        "example": "en"
      },
      "notifications_enabled": {
        "type": "boolean",
        "example": "true"
      }
    }
  }
}

IMPORTANT - JSON Structure:

The entity name (e.g., "CustomerPreferences") MUST be a direct top-level key in the JSON. Besides the entity name, the root object may only contain the access flags: "hasPersonalEntity", "personalRequiresRole", "hasPublicAccess", "hasCommunityAccess", "useRowLevelAccess" and "authMode" (see here).

Common mistake - DO NOT do this:

{
  "entity": {
    "CustomerPreferences": { ... }
  }
}

This will fail with error: "There must be 'required' field in entity"

Supported field types:

Type names are case-sensitive:

  • string
  • integer - whole numbers of any size. A value written with a decimal point, such as 1.0, is rejected.
  • number - any number. Decimals are stored as a 64-bit binary floating-point value (about 15 to 17 significant digits), so values such as 0.1 are rounded slightly. Whole numbers keep full precision.
  • boolean - true/false, or the strings "true"/"false"
  • DATE_WITH_DAY - a string in the format yyyy-MM-dd
  • json - a JSON object or array
  • reference:<EntityName> - a foreign key to another entity

For values that must be exact, such as money, use integer in minor units (for example, cents) or a string, not number.

The hasPersonalEntity flag:

When hasPersonalEntity = true (default):

OBP generates TWO sets of endpoints:

  1. Regular endpoints - Access all entities (requires specific roles)

    • POST /CustomerPreferences
    • GET /CustomerPreferences
    • GET /CustomerPreferences/ID
    • PUT /CustomerPreferences/ID
    • DELETE /CustomerPreferences/ID
  2. Personal 'my' endpoints - User-scoped access (see here)

    • POST /my/CustomerPreferences
    • GET /my/CustomerPreferences
    • GET /my/CustomerPreferences/ID
    • PUT /my/CustomerPreferences/ID
    • DELETE /my/CustomerPreferences/ID

When hasPersonalEntity = false:

OBP generates ONLY the regular endpoints. No 'my' endpoints are created. Use this when the entity represents shared data that should not be user-scoped.

Data Storage Differences:

Both personal and non-personal entities use the same database table (DynamicData), but the key difference is how user ownership is handled:

When hasPersonalEntity = true:

  • Each record stores the UserId of the user who created it
  • The UserId is actively used in all queries to filter results
  • Users can only see, update, and delete their own records via 'my' endpoints
  • The 'my' endpoints skip role checks - user isolation provides the authorization
  • Cascade delete (deleting the entity definition and all data at once) is not allowed

When hasPersonalEntity = false:

  • UserId may be stored for audit purposes but is ignored in queries
  • All authorized users see the same shared data
  • Role-based authorization is required (e.g., CanGetDynamicEntity_FooBar)
  • Cascade delete is allowed - you can delete the entity definition and all its records in one operation

Summary table:

Feature hasPersonalEntity=true hasPersonalEntity=false
Data visibility Per-user (isolated) Shared (all users)
UserId in queries Yes (filters results) No (ignored)
'my' endpoints Generated Not generated
Authorization User-scoped (no roles needed for 'my' endpoints) Role-based
Cascade delete Blocked Allowed

For bank-level entities, endpoints include the bank ID:

  • POST /banks/BANK_ID/CustomerPreferences
  • POST /banks/BANK_ID/my/CustomerPreferences (if hasPersonalEntity = true)

Auto-generated roles:

When you create a Dynamic Entity named 'FooBar', OBP automatically creates these roles:

  • CanCreateDynamicEntity_FooBar
  • CanUpdateDynamicEntity_FooBar
  • CanGetDynamicEntity_FooBar
  • CanDeleteDynamicEntity_FooBar
  • CanGrantDynamicEntityRowAccess_FooBar (only meaningful when the entity uses row-level access, see below)

Field-level write/read permissions (per property):

Each property in the schema can optionally restrict who may write or read that field, independently of the entity-level roles above:

  • write_role_required (boolean) or write_role (explicit role name) — the field becomes write-restricted: it cannot be set via POST or PUT (its existing value is preserved), only via PATCH by a caller holding the field's write role.
  • read_role_required (boolean) or read_role (explicit role name) — the field becomes read-restricted: it is omitted from GET responses unless the caller holds the field's read role (public/anonymous access omits it entirely).
  • hide_field_from_public_access (boolean) — for an entity with public access: the field is hidden from a caller whose access comes only from that public access (a caller who is not logged in, or one without the entity's read role), and shown to a caller holding the entity's read role, with no field role needed. The public endpoint always omits it.

A caller can never filter or sort by a field they may not read, whether with a plain ?field=value parameter, obp_filter, obp_sort_by, or the filter of an obp_exists join: whether a record came back would reveal the field's value. Such a request is refused with OBP-09025, naming the fields.

Restriction is on if either the boolean is true or an explicit role name is given. When a boolean is used, OBP auto-generates the role; e.g. for entity 'FooBar' field 'owner':

  • CanWriteDynamicEntityField_FooBar__owner (bank level) / CanWriteDynamicEntityField_SystemFooBar__owner (system level)
  • CanGetDynamicEntityField_FooBar__owner / CanGetDynamicEntityField_SystemFooBar__owner

Naming an explicit write_role/read_role lets several fields (even across entities) share a single role — useful for a privileged service (e.g. an indexer) that maintains many fields. Typical use: a field written only by a verifier/service or projected from an external system, but read by ordinary consumers.

Row-level access (per record):

Set useRowLevelAccess on the definition (use_row_level_access in the v6.0.0 snake_case request) to decide access per record instead of by the entity's roles. The entity's Get, Update and Delete roles then stop gating the shared routes and an access list on each record decides, carrying four permissions per User: can_read, can_update, can_delete and can_grant.

  • The User who creates a record holds all four on it, so they share their own records without holding any role. Creating a record still needs CanCreateDynamicEntity_FooBar.
  • Records the caller may not read are omitted from list responses and return 404 individually, so a record's existence is not disclosed.
  • GET /obp/dynamic-entity/FooBar/FOO_BAR_ID/access lists the access list; POST to the same URL grants or updates one entry (or an array of them) — user_id is required, can_read/can_update/can_delete default to false and can_grant defaults to true, so a grantee may re-share; DELETE /obp/dynamic-entity/FooBar/FOO_BAR_ID/access/USER_ID revokes, cascading to the grants that User passed on. Bank level entities take the same paths under /banks/BANK_ID/.
  • CanGrantDynamicEntityRowAccess_FooBar (CanGrantDynamicEntityRowAccess_SystemFooBar at system level) administers the access list of any record.
  • Field-level read and write roles still apply on top of the access list.
  • useRowLevelAccess cannot be combined with hasPublicAccess or hasCommunityAccess, and is only supported for locally-backed entities (an entity routed to an external connector for dynamicEntityProcess cannot enforce the access list).

See here for how this sits alongside the other routes.

Management endpoints:

From v7.0.0 one set of URLs manages the definitions in every space. BANK_ID is a bank's id, or SYS for the system space, and each Role is checked at that BANK_ID:

  • GET /obp/v7.0.0/management/banks/BANK_ID/dynamic-entities - List the definitions in the space, with record counts (CanGetDynamicEntityDefinitions)
  • POST /obp/v7.0.0/management/banks/BANK_ID/dynamic-entities - Create a definition (CanCreateDynamicEntityDefinition)
  • PUT /obp/v7.0.0/management/banks/BANK_ID/dynamic-entities/DYNAMIC_ENTITY_ID - Update a definition (CanUpdateDynamicEntityDefinition)
  • DELETE /obp/v7.0.0/management/banks/BANK_ID/dynamic-entities/DYNAMIC_ENTITY_ID - Delete a definition that has no records (CanDeleteDynamicEntityDefinition)
  • POST /obp/v7.0.0/management/banks/BANK_ID/dynamic-entities/DYNAMIC_ENTITY_ID/backup - Copy a definition and its records to a _BAK entity (CanBackupDynamicEntityDefinition)
  • DELETE /obp/v7.0.0/management/banks/BANK_ID/dynamic-entities/cascade/DYNAMIC_ENTITY_ID - Delete a definition and its records, after copying both to a ZZ_BAK_ entity (CanDeleteCascadeDynamicEntityDefinition)

Record endpoints from v7.0.0:

A Dynamic Entity's records are served at /obp/v7.0.0/banks/BANK_ID/dynamic-entities/..., with BANK_ID a bank's id or SYS for the system space. What follows dynamic-entities/ is the same as after /obp/dynamic-entity/:

  • ENTITY_NAME and ENTITY_NAME/RECORD_ID - list, create, read, update (PUT and PATCH) and delete
  • my/ENTITY_NAME[/RECORD_ID] - the caller's own records
  • public/ENTITY_NAME[/RECORD_ID] and community/ENTITY_NAME[/RECORD_ID] - the read-only forms
  • ENTITY_NAME/RECORD_ID/access[/USER_ID] - the access list of a row-level entity

Every v7.0.0 response carries bank_id, SYS included. The unversioned /obp/dynamic-entity/[banks/BANK_ID/]... URLs serve the same records with the same checks, and keep omitting bank_id for the system space.

Record metadata from v7.0.0:

In a v7.0.0 response each record is followed by a metadata object, which says when the record was created and last updated, and for each, which User made the call (user_id) and which User it was made for (on_behalf_of_user_id). The two ids are the same when a User acted for themselves, and differ when an agent acted for somebody through a Consent. The metadata sits beside the record rather than inside it, so it can never collide with a field of the entity:

{
  "bank_id": "SYS",
  "soil_sample": { "soil_sample_id": "8f1c2d9e-...", "ph": 6.7 },
  "metadata": {
    "created": { "at": "2026-09-30T10:12:00Z", "user_id": "a9f2c7e1-...", "on_behalf_of_user_id": "e1a4b6c8-..." },
    "updated": { "at": "2026-09-30T11:40:00Z", "user_id": "e1a4b6c8-...", "on_behalf_of_user_id": "e1a4b6c8-..." }
  }
}

In a list, each item has that same shape without bank_id: {"soil_sample": {...}, "metadata": {...}}. Times are in UTC, to the second.

  • The public reads, which need no login, carry the times only, and never say who wrote a record.
  • A value that was never recorded is null. A record written before this metadata existed has null in created for good, and updated is filled the next time the record is saved.
  • A record held by another connector (a method routing for dynamicEntityProcess names its entity) has no metadata, because OBP does not hold that information.
  • The unversioned /obp/dynamic-entity/... URLs return the record alone, and their list items stay plain records.

Earlier versions keep their separate management URLs for the system space; their Roles are the same ones, granted at SYS:

  • POST /management/system-dynamic-entities - Create system level entity
  • POST /management/banks/BANK_ID/dynamic-entities - Create bank level entity
  • GET /management/system-dynamic-entities - List all system level entities
  • GET /management/banks/BANK_ID/dynamic-entities - List bank level entities
  • PUT /management/system-dynamic-entities/DYNAMIC_ENTITY_ID - Update entity definition
  • DELETE /management/system-dynamic-entities/DYNAMIC_ENTITY_ID - Delete an entity that has no records

Discovering Dynamic Entity Endpoints (for application developers):

Once Dynamic Entities are created, their auto-generated CRUD endpoints are documented in the Resource Docs API. To programmatically discover all available Dynamic Entity endpoints, use:

GET /resource-docs/API_VERSION/obp?content=dynamic

For example: GET /resource-docs/v5.1.0/obp?content=dynamic

This returns documentation for all dynamic endpoints (both Dynamic Entities and Dynamic Endpoints) including:

  • Endpoint paths and HTTP methods
  • Request and response schemas with examples
  • Required roles and authentication
  • Field descriptions and types

You can also get this documentation in OpenAPI/Swagger format for code generation and API client tooling.

Required roles to manage Dynamic Entities:

  • CanCreateDynamicEntityDefinition, granted at the bank id of the space: SYS for the system space, or a bank's id
  • CanUpdateDynamicEntityDefinition, CanDeleteDynamicEntityDefinition, CanGetDynamicEntityDefinitions, CanBackupDynamicEntityDefinition and CanDeleteCascadeDynamicEntityDefinition, granted the same way

Use cases:

  • Customer preferences and settings
  • Custom metadata for accounts or transactions
  • Business-specific data structures
  • Rapid prototyping of new features
  • Extension of core banking data model

For user-scoped Dynamic Entities, see here

For more detailed information about managing Dynamic Entities, see here


Querying the list (GET) endpoint: filter, sort, paginate and one-hop joins

The "Get ... List" endpoint of every Dynamic Entity accepts declarative query parameters:

  • Filter: ?obp_filter[FIELD]=OP:VALUE. Operators: eq, ne, in, lt, gt, le, ge, between, like, is_null, not_set. in/between take comma-separated values; is_null/not_set take no value. Repeat the key to AND several constraints on one field.
  • Sort: ?obp_sort_by=FIELD[,FIELD2]&obp_sort_direction=ASC (or DESC).
  • Paginate: ?obp_limit=20&obp_offset=40.

Only fields declared "indexed": true are queryable. Filtering, sorting, pagination and is_null/not_set work on any deployment.

One-hop joins (EXISTS / NOT EXISTS)

You can filter one entity by a condition on a related entity that links to it through a declared reference: field — the relational "does a matching related record exist?" question:

  • ?obp_exists[child] — keep parents that have at least one related child.
  • ?obp_exists[child]=filter[status]=eq:active — parents that have at least one child matching the predicate.
  • ?obp_not_exists[child] — parents with no related child at all.
  • ?obp_not_exists[child]=filter[status]=eq:active — parents with no matching child (this includes parents that have no child at all).
  • If two entities are linked by more than one reference, disambiguate the edge with via:, e.g. ?obp_exists[child]=via:parent_ref;filter[status]=eq:active.

Mind the distinction: obp_not_exists[child]=filter[x]=eq:true (no child with x=true — includes childless parents) is NOT the same as obp_exists[child]=filter[x]=ne:true (has a child with x not equal to true — excludes childless parents).

Requirements for joins:

  • The SQL projection backend must be enabled (dynamic_entity.indexing.backend=auto on Postgres or SQL Server). On an in-memory deployment a join query returns 400 (OBP-09022); while an index is still building it returns 409.
  • The link must be a field typed reference:<Entity> AND declared "indexed": true. A plain string field holding ids is not joinable.
  • The queried (parent) entity must itself have at least one indexed field.
  • Joins are available on the authenticated and /my/ list endpoints, not on the public/community ones.

Worked example: entity parent and entity child whose field references parent

  1. Create parent with an indexed field (create it first, so reference:parent becomes a valid type):
POST /obp/v6.0.0/management/system-dynamic-entities
{ "entity_name": "parent", "has_personal_entity": false,
  "schema": { "properties": { "name": {"type":"string","example":"Acme","indexed":true} } } }
  1. Create child with a reference to parent:
POST /obp/v6.0.0/management/system-dynamic-entities
{ "entity_name": "child", "has_personal_entity": false,
  "schema": { "properties": {
    "parent_ref": {"type":"reference:parent","example":"00000000-0000-0000-0000-000000000000","indexed":true},
    "status":     {"type":"string","example":"active","indexed":true} } } }
  1. Create records. A POST /obp/v6.0.0/parent response contains a parent_id; put that value into the child's parent_ref:
POST /obp/v6.0.0/parent  {"name":"P1"}                                  -> returns parent_id (call it P1)
POST /obp/v6.0.0/child   {"parent_ref":"<P1>","status":"active"}
POST /obp/v6.0.0/child   {"parent_ref":"<P1>","status":"closed"}
  1. Query parents by a condition on their children:
GET /obp/v6.0.0/parent?obp_exists[child]=filter[status]=eq:active        -> parents that have an active child
GET /obp/v6.0.0/parent?obp_not_exists[child]=filter[status]=eq:active    -> parents with no active child
GET /obp/v6.0.0/parent?obp_exists[child]                                 -> parents that have any child
GET /obp/v6.0.0/parent?obp_not_exists[child]                             -> parents with no child at all