Schema Reference
This is the technical reference for JSON schemas that drive Raytio's dynamic form generation system (the "Wizard"). It lists every property, tag, and configuration option.
For a conceptual overview of how the schema system works, see Understanding the Schema System.
1. Schema Naming Rules
Schema names must follow this pattern:
^(p|u|s)s_[A-Za-z0-9_]+$
| Prefix | Type | Description |
|---|---|---|
ss_ | System Schema | Managed by Raytio, shared across tenants |
ps_ | Profile Schema | User-specific data schemas |
us_ | User Schema | Custom user-defined schemas |
Examples:
- ✅
ss_Driver_License - ✅
ps_My_Custom_Form - ✅
us_Company_Onboarding - ❌
driver_license(missing prefix) - ❌
ss-driver-license(hyphens not allowed)
1.1 Reserved Field Names
These field names are banned and cannot be used:
| Reserved Name | Reason |
|---|---|
encrypted_data | Internal encryption |
encrypted_key | Internal encryption |
n_id | Profile Object ID |
validation | Verification system |
encrypt | Encryption flag |
content | Media content |
content_type | MIME type |
listing_type | Legacy permissions |
permissions | Access control |
__signature | Digital signatures |
verify | Verification system |
field_list | Internal use |
breakdown | Internal use |
key | Reserved |
identity_document_picture_* | Reserved pattern |
video | Reserved |
Additional naming rules:
- Cannot contain
<=>(used for conditional field suffixes) - Cannot contain
:(used in tag syntax) - Cannot end with
_temp - Cannot start with
__group_
2. Schema Structure
2.1 Basic Template
{
"title": "Person",
"title_plural": "People",
"description": "Basic person information",
"tags": ["action:verify"],
"properties": {
"given_name": {
"type": "string",
"title": "First Name",
"tags": ["group:name:person"],
"priority": 1
},
"family_name": {
"type": "string",
"title": "Last Name",
"tags": ["group:name:person"],
"priority": 2
}
},
"required": ["given_name", "family_name"],
"verified_fields": ["given_name", "family_name"],
"display": {
"head_main": { "fields": ["given_name", "family_name"] },
"head_sub": { "fields": ["email"] }
},
"i18n": {
"en": {
"$schema": { "title": "Person", "description": "..." }
}
}
}
2.2 Schema-Level Properties
| Property | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Display name of the schema |
title_plural | string | No | Plural form (e.g., "People") |
description | string | Yes | Schema description |
properties | object | Yes | Field definitions |
required | array | No | Required field names (see Validation) |
verified_fields | array | No | Fields requiring verification (see Verification) |
tags | array | No | Schema-level behavior tags |
schema_group | string | No | Groups similar schemas (e.g., "passports") |
search_terms | string[] | No | Additional search terms/aliases for discoverability (e.g., ["license", "drivers license"]) |
schema_type | "ss"|"ps"|"us" | No | System/Profile/User schema |
schema_country_codes | string[] | No | Country availability (2-char ISO codes) |
display | object | No | Display configuration (see below) |
relationships | array | No | Related schema definitions (see Relationships) |
i18n | object | No | Localization overrides |
suggest_post_create | string | No | Suggested next schema after creation |
onboard_properties | object | No | Onboarding wizard configuration (see Onboarding Schema Guide) |
groups | object | No | Group display configuration (see Group Display Order) |
database | object | No | Database configuration for admin dashboard tables (see Database Configuration) |
2.3 Country-Restricted Schemas
Use schema_country_codes to restrict schemas to specific countries:
{
"schema_name": "ss_NZ_Driver_License",
"schema_country_codes": ["NZ"],
"schema": {
"title": "New Zealand Driver License"
}
}
| Value | Meaning |
|---|---|
["NZ"] | Only available in New Zealand |
["AU", "NZ"] | Available in Australia and New Zealand |
[] or not set | Available globally |
How it works:
- Client detects user's country (profile or browser locale)
- Only schemas matching the user's country are shown
- Empty or missing = available everywhere
2.4 Display Configuration
The display object controls how ProfileObjects appear in the UI (not the form):
{
"display": {
"head_main": {
"fields": ["given_name", "family_name"],
"format": "{given_name} {family_name}"
},
"head_sub": {
"fields": ["email"]
},
"expand": [
{
"label": "Contact Details",
"fields": ["phone", "address"]
},
{
"label": "Work Information",
"fields": ["company", "job_title"]
}
],
"compact_table": true,
"tabular": {
"fields": ["name", "email", "status"]
},
"filters": [
{
"name": "Active contacts",
"operation": "and",
"tokens": [
{ "propertyKey": "status", "operator": "=", "value": "active" }
]
}
]
}
}
| Property | Type | Description |
|---|---|---|
head_main | object | Primary header display |
head_main.fields | string[] | Fields to show in header |
head_main.format | string | Optional format string with {field} placeholders |
head_sub | object | Secondary header (subtitle) |
expand | array | Collapsible sections with labels |
compact_table | boolean | Use compact table display |
tabular | object | Table view configuration |
filters | array | Pre-defined filter presets for admin tables (see below) |
kanban | object | Kanban board configuration (see below) |
Pre-defined Filter Presets
The filters array lets schema authors provide default filter presets that appear in the admin table's "Filters" dropdown under a Default filters group. Unlike user-saved filters, these cannot be deleted by users.
Each filter maps directly to a Cloudscape PropertyFilterQuery, so propertyKey values must match keys in the schema's properties.
{
"display": {
"tabular": {
"fields": ["id", "name", "active", "start_date", "end_date"]
},
"filters": [
{
"name": "Active applications",
"operation": "and",
"tokens": [
{ "propertyKey": "active", "operator": "=", "value": "true" }
]
},
{
"name": "Guest-list enabled",
"operation": "and",
"tokens": [
{
"propertyKey": "auth_list_enabled",
"operator": "=",
"value": "true"
},
{ "propertyKey": "active", "operator": "=", "value": "true" }
]
},
{
"name": "Expired",
"operation": "and",
"tokens": [
{ "propertyKey": "active", "operator": "=", "value": "false" }
]
}
]
}
}
| Property | Type | Required | Description |
|---|---|---|---|
filters[].name | string | Yes | Display name shown in the dropdown |
filters[].operation | string | Yes | "and" (all tokens must match) or "or" (any must match) |
filters[].tokens | array | Yes | Array of filter conditions |
filters[].tokens[].propertyKey | string | Yes | Must match a key in schema properties |
filters[].tokens[].operator | string | Yes | One of: =, !=, :, !:, >, <, >=, <= |
filters[].tokens[].value | string | Yes | Value to compare against |
Operator reference:
| Operator | Meaning | Example |
|---|---|---|
= | Equals | active = true |
!= | Not equals | status != archived |
: | Contains | name : "bank" |
!: | Does not contain | name !: "test" |
> | Greater than | priority > 5 |
< | Less than | priority < 10 |
>= | Greater than or equal | start_date >= 2025-01-01 |
<= | Less than or equal | end_date <= 2025-12-31 |
How head_main is Used for Foreign Key Display
The head_main configuration is critical for foreign key ID-to-name resolution. When a field has a foreign_key_of property pointing to another schema, the client uses head_main from the foreign schema to determine what to display instead of the raw ID.
The resolution flow:
- User selects or views a foreign key field (e.g.,
party_id) - Client looks up the referenced schema (e.g.,
ss_prm_party) - Client finds the ProfileObject matching the stored ID
useMainPOFieldhook extracts thehead_main.fieldsfrom that ProfileObject- If a
formatstring exists, fields are joined using that pattern - The formatted display value is shown instead of the raw ID
Example - Foreign schema configuration:
{
"name": "ss_prm_party",
"title": "Party",
"properties": {
"party_name": { "type": "string", "title": "Party Name" },
"party_type": { "type": "string", "title": "Party Type" }
},
"display": {
"head_main": {
"fields": ["party_name", "party_type"],
"format": "{party_name} ({party_type})"
}
}
}
Example - Referencing schema:
{
"name": "ss_contract",
"properties": {
"party_id": {
"type": "string",
"title": "Party",
"foreign_key_of": "ss_prm_party"
}
}
}
Result: Instead of displaying "party-abc-123", the UI shows "Acme Corp (Customer)".
Fallback behavior:
- If no
head_mainis configured, the hook falls back to joining all available field values with spaces - If the foreign ProfileObject cannot be found, the raw ID is displayed
- If lookup substitution is configured for a
head_mainfield, the lookup value is used
Kanban Board Configuration
The kanban object configures how records are displayed in a kanban board view in the Admin Dashboard. This is used in conjunction with layout_type: "kanban" in layout_hierarchy.json.
{
"display": {
"kanban": {
"column_field": "status",
"title_field": "name",
"card_fields": ["description", "priority", "created_at"]
}
}
}
| Property | Type | Required | Description |
|---|---|---|---|
column_field | string | Yes | Field that determines which column an item belongs to |
title_field | string | Yes | Field to display as the card heading |
card_fields | string[] | No | Additional fields to display on each card |
How it works:
- The
column_fieldvalue groups records into columns. If the field has anenumdefinition, columns are created for each enum value. Otherwise, columns are created for each unique value in the data. - The
title_fieldis displayed as the card heading (falls back to "Untitled" if empty). - Each field in
card_fieldsis displayed with its label and value on the card. - Users can drag cards between columns to change the
column_fieldvalue (persisted to the database). - Users can drag columns to reorder them (order persists during the session).
Example schema with kanban configuration:
{
"name": "ss_fnd_job",
"title": "Background Job",
"properties": {
"name": { "type": "string", "title": "Job Name" },
"status": {
"type": "string",
"title": "Status",
"enum": ["pending", "running", "completed", "failed"]
},
"program_name": { "type": "string", "title": "Program" },
"scheduled_at": {
"type": "string",
"format": "date-time",
"title": "Scheduled"
}
},
"display": {
"kanban": {
"column_field": "status",
"title_field": "name",
"card_fields": ["program_name", "scheduled_at"]
}
}
}
Priority order for kanban configuration:
currentView.kanban_configfromavailable_layouts(for multi-view layouts)- Schema's
display.kanbanproperty (recommended) layout_hierarchy.jsonkanban_configproperty (legacy/fallback)- Auto-detection: looks for
status/state/labelfields for columns,title/namefor card title
Note: To enable kanban view for a schema in the Admin Dashboard, you must also set layout_type: "kanban" in the corresponding entry in layout_hierarchy.json, or configure available_layouts for multi-view support. See the layout_hierarchy.json configuration for details.
2.5 Group Display Order
The groups object provides configuration options for field groups.
{
"title": "Party",
"properties": {
"name": {
"type": "string",
"tags": ["group:summary"],
"priority": 1
},
"email": {
"type": "string",
"tags": ["group:summary"],
"priority": 2
},
"internal_notes": {
"type": "string",
"tags": ["group:advanced"],
"priority": 1
}
},
"groups": {
"order": ["summary", "advanced"]
}
}
Groups Properties
| Property | Type | Description |
|---|---|---|
order | string[] | Array of group names specifying their display order |
How Group Ordering Works
- Specified groups first: Groups listed in the
orderarray appear first, in the exact order specified - Unspecified groups after: Groups NOT in the
orderarray appear after ordered groups, sorted by the priority of their first child field - Backwards compatible: If
groups.orderis not specified, all groups sort by their first child's priority (original behavior)
Example:
{
"groups": {
"order": ["summary", "contact"]
}
}
With this configuration:
- "summary" group appears first
- "contact" group appears second
- Any other groups (e.g., "advanced", "notes") appear after, sorted by priority
Special Groups
| Group Name | Description |
|---|---|
-notag-common | Fields without any group tag (can be positioned) |
Note: The -notag-common group collects fields that don't have a group:* tag. You can include it in the order array to position ungrouped fields.
2.6 Database Configuration
The database object configures how schemas interact with database tables in the Admin Dashboard. This is used for schemas that represent database records (as opposed to Profile Objects).
{
"database": {
"path": "/db/v1/rpc/my_function",
"primary_key": "id",
"select": "id,name,email,created_at",
"return_to": "ss_my_view_schema"
}
}
Database Properties
| Property | Type | Required | Description |
|---|---|---|---|
path | string | No* | Complete API path (e.g., /db/v1/my_table or /db/v1/rpc/my_function) |
table | string | No* | Table name, used to construct /db/v1/{table} when path is not specified |
primary_key | string | Yes | Primary key column name (e.g., id) |
select | string | No | PostgREST select parameter for specifying columns (e.g., id,name,email) |
query_parameters | string | No | PostgREST query parameters with variable support (see below) |
return_to | string | No | Schema name to redirect to after creating a record (for insert schemas) |
method | "POST" | "PATCH" | "DELETE" | No | HTTP method override. Use POST for RPC endpoints (default varies by action) |
* Either path or table must be specified, but not both. If path is specified, it is used directly; otherwise /db/v1/{table} is constructed.
Path vs Table
Use path when:
- Calling PostgreSQL RPC functions (e.g.,
/db/v1/rpc/my_insert_function) - Using non-standard API paths
- Need full control over the endpoint
Use table when:
- Accessing standard PostgREST tables
- The endpoint follows the
/db/v1/{table}convention
Examples:
// Using path for RPC function
{
"database": {
"path": "/db/v1/rpc/dsm_access_application_users_i",
"primary_key": "id",
"return_to": "ss_dsm_access_application_user"
}
}
// Using table for standard table access
{
"database": {
"table": "dsm_access_application_users",
"primary_key": "id",
"select": "id,email,aa_id,active"
}
}
The return_to Property
Use return_to for "insert" schemas that create records via RPC functions. After successful creation, the Admin Dashboard redirects to the specified view schema showing the new record.
Use case: When you have separate schemas for viewing vs inserting records:
- Insert schema (
ss_dsm_access_application_user_i): Calls RPC function with parametersp_aa_id,p_email - View schema (
ss_dsm_access_application_user): Displays the full record from a database view
// Insert schema
{
"schema_name": "ss_dsm_access_application_user_i",
"database": {
"path": "/db/v1/rpc/dsm_access_application_users_i",
"primary_key": "id",
"return_to": "ss_dsm_access_application_user"
},
"properties": {
"p_aa_id": {
"type": "string",
"foreign_key_of": "ss_dsm_access_application"
},
"p_email": { "type": "string", "title": "Email address" }
},
"required": ["p_aa_id", "p_email"]
}
After creating a record, the user is redirected from /admin/ss_dsm_access_application_user_i/add to /admin/ss_dsm_access_application_user/{new_id}.
Query Parameters
The query_parameters property allows specifying additional PostgREST query parameters that are included in all read requests. This is useful for:
- Filtering data at the database level (e.g.,
active=eq.true) - Specifying complex select statements with nested resources
- Adding organization or tenant scoping
Variable Substitution:
Query parameters support variable substitution using the {$context.X} syntax:
| Variable | Description |
|---|---|
{$context.organizationId} | Current organization ID |
{$context.userId} | Current user ID |
{$context.tenantId} | Current tenant ID |
Example:
{
"database": {
"table": "price_lists",
"primary_key": "id",
"query_parameters": "org_id=eq.{$context.organizationId}&active=eq.true&select=*,versions(*,lines(*))"
}
}
This generates URLs like:
/db/v1/price_lists?org_id=eq.org-123&active=eq.true&select=*,versions(*,lines(*))&limit=20&offset=0
Precedence:
- If
query_parameterscontainsselect=, the separateselectproperty is ignored query_parametersvalues override auto-generated parameters (filters, sorting) with the same key- Pagination (
limit,offset) is always appended unless overridden inquery_parameters
3. Field Reference
3.1 Field Templates
The SchemaEditor provides these pre-built field templates. Use these as starting points:
Text Field
{
"field_name": {
"type": "string",
"title": "Field Label"
}
}
Date Field
{
"date_field": {
"type": "string",
"format": "date",
"title": "Select Date"
}
}
DateTime Field
{
"datetime_field": {
"type": "string",
"format": "date-time",
"title": "Select Date and Time"
}
}
Number Field
{
"number_field": {
"type": "number",
"title": "Amount",
"minimum": 0,
"maximum": 1000
}
}
Money Field
{
"price": {
"type": "string",
"title": "Price",
"tags": ["display:currency"]
}
}
Star Rating Field
{
"rating": {
"type": "string",
"title": "Rating",
"tags": ["display:stars"]
}
}
Select (Dropdown) Field
{
"status": {
"type": "string",
"title": "Status",
"enum": ["active", "pending", "inactive"]
}
}
Multi-Select Field
{
"categories": {
"type": "array",
"title": "Categories",
"enum": ["tech", "finance", "health", "education"]
}
}
Checkbox Field
{
"agree_terms": {
"type": "boolean",
"title": "I agree to the terms",
"content": "I have read and agree to the Terms of Service"
}
}
Multi-Checkbox Field
{
"preferences": {
"type": "array",
"title": "Preferences",
"items": {
"type": "boolean",
"properties": {}
},
"lookup": "https://api.rayt.io/lookups/preferences.json"
}
}
File Upload Field
{
"document": {
"type": "string",
"title": "Upload Document",
"contentEncoding": "base64",
"tags": ["action:client_upload"]
}
}
Content Block (Display-Only Guidance)
Content blocks display markdown-formatted text to guide users through forms. They are not form inputs and are excluded from form submission.
Plain markdown (no styling):
{
"intro_text": {
"type": "string",
"title": "Getting Started",
"description": "Fill out the fields below. **Required fields** are marked with an asterisk.",
"tags": ["display:content-block"],
"priority": 10
}
}
Info alert (blue):
{
"help_info": {
"type": "string",
"title": "Helpful Information",
"description": "This is an **info** block for general guidance.",
"tags": ["display:content-block", "display:info"],
"priority": 20
}
}
Warning alert (orange):
{
"caution_notice": {
"type": "string",
"title": "Please Note",
"description": "This is a **warning** block for important notices.",
"tags": ["display:content-block", "display:warning"],
"priority": 30
}
}
Error alert (red):
{
"critical_notice": {
"type": "string",
"title": "Critical Information",
"description": "This is an **error** block for critical information.",
"tags": ["display:content-block", "display:error"],
"priority": 40
}
}
Key points:
- Use
tags: ["display:content-block"]to create a content block (withtype: "string"for JSON Schema compliance) - The
descriptionfield contains the markdown content - Use
priorityto position the content block among other fields - Add
display:info,display:warning, ordisplay:errortags for styled variants - Content blocks can be placed within groups using the
group:*tag - Content blocks are automatically excluded from form submission
3.2 Core Properties
| Property | Type | Default | Description |
|---|---|---|---|
type | string | - | Data type: string, number, integer, boolean, array, object |
title | string | - | Field label |
title_plural | string | - | Plural form of title |
description | string | - | Help text below field |
default | any | - | Default value (see below for special cases) |
example | any | - | Example value (for documentation) |
readOnly | boolean | false | Hide from wizard form |
encrypt | boolean | false | Encrypt field value when storing |
encrypt_require | boolean | false | User must enable encryption to save |
tags | array | [] | Field behavior tags |
priority | number | 0 | Display order (lower = earlier) |
The default Property
The default property sets the initial value for a field. Its behavior varies by field type:
Standard Fields (string, number, boolean)
{
"status": {
"type": "string",
"default": "active"
},
"quantity": {
"type": "number",
"default": 1
},
"subscribe": {
"type": "boolean",
"default": true
}
}
Date Fields (format: "date")
For date fields, default is a number representing days from today:
{
"start_date": {
"type": "string",
"format": "date",
"default": 0
},
"expiry_date": {
"type": "string",
"format": "date",
"default": 90
}
}
| Default Value | Result |
|---|---|
0 | Today's date |
7 | 7 days from now |
90 | 90 days from now |
-30 | 30 days ago |
| Not set | Field remains empty |
DateTime Fields (format: "date-time")
For datetime fields, default is a number representing seconds from now:
{
"scheduled_at": {
"type": "string",
"format": "date-time",
"default": 3600
},
"expires_at": {
"type": "string",
"format": "date-time",
"default": 604800
}
}
| Default Value | Result |
|---|---|
0 | Current date/time |
3600 | 1 hour from now (60×60) |
86400 | 24 hours from now (60×60×24) |
604800 | 7 days from now (60×60×24×7) |
| Not set | Field remains empty |
Birth Date Fields (with group:date_picker:birth tag)
Birth date fields automatically default to today's date components when the date picker is used. No default is needed.
Important: No Default = Empty Field
If no default is specified for date/datetime fields, the field remains empty.
Previously, date fields without a default would auto-populate with the current date, causing issues when those fields were sent to the backend unexpectedly.
// ❌ OLD BEHAVIOR (before fix): date_created would auto-fill with today
{
"date_created": {
"type": "string",
"format": "date-time",
"readOnly": true
}
}
// ✅ CURRENT BEHAVIOR: date_created stays empty unless default is set
// This is correct for server-generated timestamps
When to use default on date fields:
- User-facing dates that should pre-fill (e.g., "Start Date" defaulting to today)
- Expiry dates with a standard validity period
When NOT to use default:
- Server-generated timestamps (
date_created,date_updated) - Fields that should remain empty until user input
readOnlydate fields populated by the backend
3.3 Type-Specific Properties
String Fields
| Property | Type | Description |
|---|---|---|
enum | array | Allowed values (≤7 = radio buttons, >7 = select) |
pattern | string | Regex validation pattern |
patternMessage | string | Custom error for pattern failure |
minLength | number | Minimum string length |
maxLength | number | Maximum length (>30 = textarea) |
format | string | Special format: date, date-time, uri |
lookup | string | URL to lookup JSON for dropdown |
Number Fields
| Property | Type | Description |
|---|---|---|
minimum | number | Minimum value |
maximum | number | Maximum value |
step | number | Increment step |
enum | array | Specific allowed values |
Array Fields
| Property | Type | Description |
|---|---|---|
items | object | Definition of array items |
items.type | string | Type of items |
items.properties | object | For object arrays: nested fields |
enum | array | For multi-select from fixed options |
Media/File Fields
| Property | Type | Description |
|---|---|---|
contentMediaType | string | MIME type (e.g., "image/jpeg") |
contentEncoding | "base64" | Encoding specification |
content_source | string | Image source configuration |
image_silhouette | string | URL to camera overlay image |
Table Fields (for array of objects)
| Property | Type | Description |
|---|---|---|
add_row_btn_label | string | "Add Row" button text |
table_empty_message | string | Message when table is empty |
Reference Fields
| Property | Type | Description |
|---|---|---|
$ref | string | Reference to sub-object schema |
foreign_key_of | string | FK reference to another schema |
The foreign_key_of Property
The foreign_key_of property creates a relationship between a field and another schema, enabling ID-to-name resolution in both forms and display views.
Basic usage:
{
"customer_id": {
"type": "string",
"title": "Customer",
"foreign_key_of": "ts_customer"
}
}
What happens in forms (editing):
- The field renders as a searchable dropdown (
ForeignKeySelectcomponent) - The dropdown is populated with all ProfileObjects of the referenced schema
- Each option's label is determined by the foreign schema's
display.head_mainconfiguration - The stored value is the ProfileObject's
n_id(the actual ID)
What happens in display (viewing):
- The field renders as a link (
DisplayForeignKeycomponent) - The link text shows the resolved name from
head_main, not the raw ID - Clicking the link navigates to the referenced ProfileObject
- If the foreign PO has a
descriptionproperty, it appears as a tooltip
Configuration requirements:
- The referencing field needs
foreign_key_ofpointing to the schema name - The referenced schema should have
display.head_mainconfigured for meaningful display
Complete example:
// Referenced schema (ts_customer)
{
"name": "ts_customer",
"title": "Customer",
"properties": {
"customer_name": { "type": "string", "title": "Name" },
"customer_code": { "type": "string", "title": "Code" },
"description": { "type": "string", "title": "Description" }
},
"display": {
"head_main": {
"fields": ["customer_name", "customer_code"],
"format": "{customer_name} ({customer_code})"
}
}
}
// Referencing schema (ts_order)
{
"name": "ts_order",
"title": "Order",
"properties": {
"order_number": { "type": "string", "title": "Order Number" },
"customer_id": {
"type": "string",
"title": "Customer",
"foreign_key_of": "ts_customer"
}
},
"display": {
"head_main": { "fields": ["order_number"] }
}
}
Result:
- Form shows a dropdown with options like "Acme Corp (ACME001)", "Beta Inc (BETA002)"
- Stored value:
"customer-uuid-123" - Display shows: "Acme Corp (ACME001)" as a clickable link
3.4 ReadOnly Behavior
The readOnly property controls field visibility in the Wizard:
Key rule: If ALL fields in a group have readOnly: true, the ENTIRE group is hidden.
Problem scenario:
{
"payment_id": { "readOnly": true, "tags": ["group:stripe_elements"] },
"card_last4": { "readOnly": true, "tags": ["group:stripe_elements"] },
"card_brand": { "readOnly": true, "tags": ["group:stripe_elements"] }
}
↑ ALL fields are readOnly = Stripe UI component is HIDDEN!
Solution: At least one field must NOT be readOnly:
{
"payment_id": { "tags": ["group:stripe_elements"] },
"card_last4": { "readOnly": true, "tags": ["group:stripe_elements"] },
"card_brand": { "readOnly": true, "tags": ["group:stripe_elements"] }
}
When to use readOnly: true:
| Use Case | Use readOnly? |
|---|---|
| System-generated fields (IDs, timestamps) | Yes |
| Fields populated by API extraction | Yes |
| Display-only fields | Yes |
| Fields in specialized DynamicSections | At least one must be false |
| Fields the user should edit | No |
4. Field Tags Reference
Field tags control rendering behavior. Format: "category:value" or "category:sub:value".
Group Tags (Determines Rendering Component)
| Tag | Triggers | Description |
|---|---|---|
group:address | AddressPickerDynamicSection | Google Places autocomplete |
group:name:* | PersonNameDynamicSection | Combined name picker |
group:date_picker:* | DatePickerDynamicSection | Date picker (day/month/year) |
group:date_picker:birth | DatePickerDynamicSection | Birth date with age validation |
group:images | ImageDynamicSection | Document capture + OCR |
group:display_qr_code | QrCodeDynamicSection | QR code scanning flow |
group:camera | CameraDynamicSection | Photo capture |
group:yodlee-accounts | YodleeDynamicSection | Yodlee banking |
group:akahu-oauth | AkahuDynamicSection | NZ banking (Akahu) |
group:oauth2 | OAuthDynamicSection | OAuth 2.0 flow |
group:stripe_elements | StripeDynamicSection | Stripe payment |
group:covid | CovidDynamicSection | COVID pass scanning |
group:selectProfileObject:* | SelectPODynamicSection | Select existing PO |
group:findProfileObject:* | SelectPODynamicSection | Link to existing PO |
group:autocomplete | AutoCompleteDynamicSection | Autocomplete input |
upload-group:* | - | File upload grouping |
Date Component Tags
| Tag | Description |
|---|---|
date_component:day | Day field in date picker group |
date_component:month | Month field in date picker group |
date_component:year | Year field in date picker group |
Display Tags
| Tag | Description |
|---|---|
display:content-block | Marks field as a display-only content block |
display:info | Content block variant: blue info alert |
display:warning | Content block variant: orange warning alert |
display:error | Content block variant: red error alert |
display:stars | Render as star rating |
display:no_autofill | Disable browser autofill |
display:currency | Format as currency |
display:cascade | Cascading/hierarchical select |
display:survey | Survey mode (rating buttons) |
display:quoting | Quote display mode |
display:customModal | Custom modal presentation |
display:mask | Mask input (password-style) |
display:replace:('old', 'new') | String replacement display |
display:terms_conditions | Terms checkbox styling |
display:main_media:propName | Main media source property |
Action Tags
| Tag | Description |
|---|---|
action:allow_copy | Add clipboard copy button |
action:allow_unreplace | Add reveal button for masked fields |
action:allow_password_compromise_check | Check if password is breached |
action:client_upload | Client-side file upload |
action:hash | Create argon hash (use with hash_inputs) |
action:require_webauthn | Require WebAuthN/MFA |
action:generate_pepper:N | Generate N-byte random pepper |
action:timeout:N | Auto-refresh lookup after N seconds (see 11.7) |
action:use_$ref:source.property | Pre-fill from selected PO (see below) |
The action:use_$ref Tag
Pre-fill fields from a selected ProfileObject:
Format: action:use_$ref:[selectPO_field].[property]
{
"target_document": {
"type": "string",
"tags": ["group:selectProfileObject:ss_Identity"]
},
"first_name": {
"type": "string",
"tags": [
"group:name:person",
"action:use_$ref:target_document.first_given_name"
]
}
}
When user selects an identity document, first_name is auto-populated from the selected PO's first_given_name property.
Verification Tags
| Tag | Description |
|---|---|
verify:show_if_pending | Only show during pending verification |
Type Tags
| Tag | Description |
|---|---|
type:capture_geolocation | Capture GPS coordinates |
type:extract_required | Extract from linked PO |
type:extract_required:prop1,prop2 | Extract specific properties |
5. Schema Tags Reference
Schema-level tags control form-wide behavior.
Action Tags
| Tag | Description |
|---|---|
action:verify | Enable verification (requires verified_fields) |
action:experimental_pass_object_store_id | Pass object store ID to API |
action:display_qr_code:extract:start | QR code extraction at start |
action:display_qr_code:verify:pending | QR code for pending verification |
action:display_global_idv_app_qr_code:extract | Global IDV app extraction |
action:reverify:display_qr_code | Enable QR code reverification flow |
Camera Tags
| Tag | Description |
|---|---|
default_camera:rear | Default to rear camera |
default_camera:front | Default to front camera |
OAuth Tags
| Tag | Description |
|---|---|
oauth2_component:name:ProviderName | OAuth provider display name |
oauth2_component:redirect_url:URL | OAuth redirect URL |
Timing Tags
| Tag | Description |
|---|---|
time:extract:30 | Estimated extraction time (seconds) |
time:verify_pending_delay:60 | Verification delay (seconds) |
time:live_person:45 | Live person verification time |
Link Tags
| Tag | Description |
|---|---|
link_to:ss_Schema:relationship_type | Define schema relationships |
Type Tags
| Tag | Description |
|---|---|
type:service_provider | Service provider schema |
type:service_offer | Service offer schema |
type:marketplace | Marketplace schema |
type:client_only | Client-side only (not stored) |
type:globally_unique_field | Contains globally unique fields |
type:application_object | Application object (skips verification) |
Display Tags
| Tag | Description |
|---|---|
display:default_view:edit | Link to edit mode from profile |
6. Relationships
Relationships define connections between schemas. Configure in the relationships array:
{
"relationships": [
{
"relationship_name": "identity_documents",
"type": "owns",
"direction": "from",
"oneOf": ["ss_Passport", "ss_Driver_License"],
"multiple": true,
"required_relationship": false
},
{
"relationship_name": "employer",
"type": "works_at",
"direction": "from",
"anyOf": ["ss_Company", "instance"],
"multiple": false,
"required_relationship": true
}
]
}
Relationship Properties
| Property | Type | Description |
|---|---|---|
relationship_name | string | Unique identifier for the relationship |
type | string | Relationship classification (e.g., "owns", "works_at") |
direction | "from" | Always "from" (direction of relationship) |
oneOf | string[] | User must select ONE of these schemas |
anyOf | string[] | User can select ANY of these schemas |
multiple | boolean | Allow multiple relationships (default: true) |
required_relationship | boolean | Must complete before sharing |
properties | object | Custom fields on the relationship |
required | string[] | Required relationship fields |
Special Value: "instance"
Use "instance" to allow linking to ANY ProfileObject:
{
"anyOf": ["ss_Company", "instance"]
}
This allows the user to either select a Company schema OR any other PO they have.
7. Dynamic Sections
Dynamic Sections are specialized React components that render groups of fields. The system matches fields by their group:* tags.
How Matching Works
- Fields are grouped by their
group:*tag prefix - Each DynamicSection has a
GroupRulewith a priority value - Higher priority wins (most specific match)
- Default priority is
0(catches unmatched fields)
Section Reference
AddressPickerDynamicSection
Trigger: group:address*
Priority: 1
Google Places autocomplete with manual entry fallback.
{
"street_number": {
"type": "string",
"tags": ["group:address"],
"priority": 1
},
"route": { "type": "string", "tags": ["group:address"], "priority": 2 },
"locality": { "type": "string", "tags": ["group:address"], "priority": 3 },
"country": { "type": "string", "tags": ["group:address"], "priority": 4 },
"postal_code": { "type": "string", "tags": ["group:address"], "priority": 5 }
}
Supported fields: street_number, route, subpremise, premise, sublocality, sublocality_level_1-5, locality, country, postal_code, postal_code_suffix, administrative_area_level_1-5, geolocation
DatePickerDynamicSection
Trigger: group:date_picker*
Priority: 1
Single date picker UI synced to hidden day/month/year fields.
{
"birth_day": {
"type": "integer",
"minimum": 1,
"maximum": 31,
"tags": ["group:date_picker:birth", "date_component:day"]
},
"birth_month": {
"type": "integer",
"minimum": 1,
"maximum": 12,
"tags": ["group:date_picker:birth", "date_component:month"]
},
"birth_year": {
"type": "integer",
"minimum": 1900,
"maximum": 2024,
"tags": ["group:date_picker:birth", "date_component:year"]
}
}
ImageDynamicSection
Trigger: group:images* (create mode only)
Priority: 1
Document capture with OCR extraction.
{
"tags": ["action:verify"],
"properties": {
"front_image": {
"type": "string",
"format": "uri",
"contentMediaType": "image/jpeg",
"contentEncoding": "base64",
"tags": ["group:images"],
"image_silhouette": "https://assets.rayt.io/silhouettes/license-front.png"
},
"extracted_name": {
"type": "string",
"tags": ["group:images"],
"readOnly": true
}
}
}
WizardPage threshold options:
extract_threshold: Confidence threshold (0-1)extract_threshold_pass_action:"review"|"next_step"|"capture"|"capture_review"extract_threshold_fail_action: Same optionsextract_threshold_null_action: Same options
QrCodeDynamicSection
Trigger: group:display_qr_code
Priority: 1
Multi-step QR code flow for document verification.
{
"tags": ["action:display_qr_code:extract:start"],
"properties": {
"identity_document_type": {
"type": "string",
"enum": ["P", "D"],
"tags": ["group:display_qr_code"]
}
}
}
Reverification Flow:
To enable QR code reverification for an existing ProfileObject, add the action:reverify:display_qr_code schema tag:
{
"tags": [
"action:verify",
"action:display_qr_code:extract:start",
"action:reverify:display_qr_code"
]
}
When this tag is present:
- Users can reverify existing POs via QR code scan
- All existing verifications for the PO are expired before creating a new one
PersonNameDynamicSection
Trigger: group:name*
Priority: 1
Combined name picker from existing profile objects.
{
"given_name": {
"type": "string",
"tags": ["group:name:person"],
"priority": 1
},
"middle_name": {
"type": "string",
"tags": ["group:name:person"],
"priority": 2
},
"family_name": {
"type": "string",
"tags": ["group:name:person"],
"priority": 3
}
}
CameraDynamicSection
Trigger: group:camera
Priority: 2
Photo capture via webcam.
{
"tags": ["default_camera:front"],
"properties": {
"selfie": {
"type": "string",
"format": "uri",
"contentMediaType": "image/jpeg",
"tags": ["group:camera"]
}
}
}
SelectPODynamicSection
Trigger: group:selectProfileObject:* or group:findProfileObject:*
Priority: 1
Displays the user's existing ProfileObjects (of the specified schema type) as selectable radio cards. Useful when a form needs to reference data the user has already saved.
{
"linked_identity": {
"type": "object",
"tags": ["group:selectProfileObject:ss_Global_Identity_Document"]
}
}
- If only one matching PO exists, it auto-selects
- If none exist, shows a "Create" button linking to that schema
- Works with
action:use_$reftag to pre-fill other fields from the selected PO
Other Dynamic Sections
| Section | Trigger | Description |
|---|---|---|
| YodleeDynamicSection | group:yodlee-accounts | Yodlee bank linking |
| AkahuDynamicSection | group:akahu-oauth | NZ bank linking |
| OAuthDynamicSection | group:oauth2* | Generic OAuth flow |
| StripeDynamicSection | group:stripe_elements | Stripe payment |
| CovidDynamicSection | group:covid | COVID pass scanning |
8. Conditional Logic
8.1 Which Approach to Use
┌─────────────────────────────┐
│ Do you need different field │
│ DEFINITIONS per condition? │
│ (enum, validation, tags) │
└─────────────────────────────┘
│
┌──────────┴──────────┐
│ │
YES NO
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Schema-Level │ │ Field-Level │
│ allOf with │ │ if property │
│ if/then │ │ │
└──────────────────┘ └──────────────────┘
Creates separate Simple show/hide
fields with Same field definition
<=> suffix
| Feature | Field-Level if | Schema-Level allOf |
|---|---|---|
| Where defined | On individual field | At schema root |
| What it controls | Visibility only | Properties, enums, required, verified_fields |
| Creates new fields | No | Yes (with <=> condition suffix) |
| Use case | Simple show/hide | Different field definitions per condition |
8.2 Field-Level Conditions (Simple)
Use if directly on a field for simple show/hide:
{
"document_type": {
"type": "string",
"enum": ["passport", "driver_license"]
},
"passport_number": {
"type": "string",
"if": { "document_type": ["passport"] }
},
"license_number": {
"type": "string",
"if": { "document_type": ["driver_license"] }
}
}
Logic: Field shown if trigger field equals ANY of the listed values.
Multiple conditions (AND):
{
"special_field": {
"if": {
"document_type": ["passport"],
"country": ["NZ", "AU"]
}
}
}
This means: (document_type = "passport") AND (country = "NZ" OR country = "AU")
8.3 Schema-Level allOf
Use allOf when you need different field definitions per condition:
{
"properties": {
"identity_document_type": {
"type": "string",
"enum": ["D", "P"],
"tags": ["group:display_qr_code"]
},
"identity_document_data_source": {
"type": "string",
"enum": ["chip", "app", "camera"],
"tags": ["group:display_qr_code"]
}
},
"allOf": [
{
"if": { "properties": { "identity_document_type": { "enum": ["P"] } } },
"then": {
"properties": {
"identity_document_data_source": {
"enum": ["chip"]
}
}
}
},
{
"if": { "properties": { "identity_document_type": { "enum": ["D"] } } },
"then": {
"properties": {
"identity_document_data_source": {
"enum": ["chip", "app"]
}
}
}
}
]
}
What happens:
- Build time: Creates conditional fields with
<=> conditionsuffix - Runtime: Shows appropriate field based on current selection
The <=> condition suffix format:
fieldName <=> trigger1=value1 & trigger2=value2|value3
8.4 Conditional Required Fields
Make fields conditionally required:
{
"required": [
"always_required_field",
{
"field": "sometimes_required",
"if": { "trigger_field": ["trigger_value"] }
}
]
}
Example:
{
"properties": {
"has_middle_name": { "type": "boolean" },
"middle_name": { "type": "string" }
},
"required": [
{
"field": "middle_name",
"if": { "has_middle_name": [true] }
}
]
}
8.5 Runtime Conditional Evaluation
The client supports runtime evaluation of conditional schema properties and tags. This includes:
Conditional Tags
Conditional tags in allOf blocks are now evaluated at runtime based on current form values:
{
"allOf": [
{
"if": {
"properties": { "identity_document_type": { "enum": ["EU-PID"] } }
},
"then": {
"tags": ["action:display_global_idv_app_qr_code:extract"]
}
}
]
}
Behavior: The tag action:display_global_idv_app_qr_code:extract is only active when identity_document_type === "EU-PID". This enables conditional UI behaviors like showing App Store QR codes only for specific document types.
Conditional Field Properties
Conditional field properties (title, enum, description, lookup) are merged at runtime:
{
"properties": {
"identity_document_data_source": {
"type": "string",
"title": "Data Source",
"enum": ["app", "chip", "nfc"]
}
},
"allOf": [
{
"if": { "properties": { "identity_document_type": { "enum": ["P"] } } },
"then": {
"properties": {
"identity_document_data_source": {
"title": "PASSPORT: App or Chip Reader",
"enum": ["app", "chip"]
}
}
}
},
{
"if": { "properties": { "identity_document_type": { "enum": ["D"] } } },
"then": {
"properties": {
"identity_document_data_source": {
"title": "DRIVERS LICENSE: NFC Only",
"enum": ["nfc"]
}
}
}
}
]
}
Behavior: When the user selects "P" (Passport), the data source field shows title "PASSPORT: App or Chip Reader" with only "app" and "chip" options. When "D" (Driver License) is selected, it shows "DRIVERS LICENSE: NFC Only" with only "nfc".
How It Works
The client uses evaluateSchemaConditions() to process allOf blocks at runtime:
- Form values trigger re-evaluation when they change
- Matching conditionals have their
thenproperties deep-merged into the base schema - Tags from matching conditionals are concatenated with base tags
- Components access the evaluated schema instead of the static schema
9. Verification
9.1 When /verify is Called
The /verify API is called only when ALL conditions are true:
- ✅ Schema has
action:verifytag - ✅
verified_fieldsarray is NOT empty - ✅
dontVerifyis NOT set (WizardPage config) - ✅ At least one verifiable field has a value
Common mistake: Having action:verify but no verified_fields:
// ❌ WRONG - /verify will NOT be called
{
"tags": ["action:verify"],
"properties": { "doc_number": { "type": "string" } }
}
// ✅ CORRECT - /verify WILL be called
{
"tags": ["action:verify"],
"properties": { "doc_number": { "type": "string" } },
"verified_fields": ["doc_number"]
}
Disabling Verification
Per WizardPage:
{
"pages": [
{
"schemas": ["ss_Driver_License"],
"verify_data": false
}
]
}
Application Objects: Schemas with type:application_object tag automatically skip verification.
9.2 Conditional Verification
Make verification conditional using allOf:
{
"tags": ["action:verify"],
"properties": {
"verification_level": {
"type": "string",
"enum": ["basic", "enhanced"]
},
"document_number": { "type": "string" },
"expiry_date": { "type": "string", "format": "date" }
},
"verified_fields": ["document_number"],
"allOf": [
{
"if": {
"properties": { "verification_level": { "enum": ["enhanced"] } }
},
"then": {
"verified_fields": ["document_number", "expiry_date"]
}
}
]
}
Result:
verification_level = "basic"→ Onlydocument_numberverifiedverification_level = "enhanced"→ Both fields verified
10. Validation
10.1 Required Fields
Always required:
{
"required": ["field1", "field2"]
}
Conditionally required:
{
"required": [
"always_required",
{ "field": "sometimes_required", "if": { "trigger": ["value"] } }
]
}
10.2 String Validation
{
"license_number": {
"type": "string",
"pattern": "^[A-Z]{2}\\d{6}$",
"patternMessage": "Must be 2 letters + 6 digits (e.g., AB123456)",
"minLength": 8,
"maxLength": 8
}
}
10.3 Number Validation
{
"age": {
"type": "integer",
"minimum": 0,
"maximum": 150
},
"price": {
"type": "number",
"minimum": 0.01,
"maximum": 1000000,
"step": 0.01
}
}
10.4 Boolean Validation (Must Accept)
When a boolean is required, user must check the checkbox:
{
"properties": {
"accept_terms": {
"type": "boolean",
"content": "I accept the terms and conditions"
}
},
"required": ["accept_terms"]
}
11. Lookups
11.1 Enum vs Lookup
Both enum and lookup provide options for selection fields, but they work differently:
| Feature | enum | lookup |
|---|---|---|
| Data source | Inline in schema | External URL (JSON file) |
| Display values | Key = display value | Separate key and value properties |
| Extra data | None | description, image_src, etc. |
| Rendering | ≤7 = radio buttons, >7 = dropdown | Always searchable dropdown |
| Caching | N/A | Cached by URL |
When to Use Each
| Scenario | Use |
|---|---|
| Small fixed list (yes/no, status) | enum |
| Large list (countries, categories) | lookup |
| Need display labels ≠ stored values | lookup |
| Need descriptions or icons | lookup |
| Shared options across schemas | lookup |
| Quick prototype | enum |
| Options change without schema updates | lookup |
11.2 How Enum and Lookup Interact
In standard form fields, enum and lookup are alternatives, not combined.
Priority order for standard dropdowns:
- If
lookupis present → use lookup options (enum is ignored) - If no
lookupbutenumexists → use enum values as options
Practical implication:
// ❌ This does NOT filter lookup by enum in standard dropdowns
{
"country": {
"type": "string",
"enum": ["NZ", "AU"],
"lookup": "https://api.rayt.io/lookups/countries.json"
}
}
// Result: Shows ALL countries from lookup (enum is ignored)
// ✅ Use only enum for fixed options
{
"country": {
"type": "string",
"enum": ["NZ", "AU"]
}
}
// Result: Shows "NZ" and "AU" as both key and display value
// ✅ Use only lookup for dynamic options with display values
{
"country": {
"type": "string",
"lookup": "https://api.rayt.io/lookups/countries.json"
}
}
// Result: Shows all countries with proper display names
Enum and Lookup Interaction (All Form Fields)
When both enum and lookup are defined on a field, the following behavior applies consistently across all form fields (standard Select fields and QrCodeDynamicSection's SelectionFlow):
Behavior:
- If
enumvalues exist in the lookup → show only matching lookup items (filtered) - If
enumvalues do NOT exist in the lookup → show ALL lookup items (fallback) - If only
lookupis defined → show all lookup items - If only
enumis defined → show enum values as options
This ensures that invalid enum values don't result in showing raw enum keys. Instead, the full lookup is displayed, which provides a better user experience when there's a schema configuration mismatch.
Example:
{
"country": {
"type": "string",
"enum": ["NZ", "AU"],
"lookup": "{API_DOCS_URL}/lookups/countries.json"
}
}
If countries.json contains [{key: "NZ", value: "New Zealand"}, {key: "AU", value: "Australia"}, {key: "US", value: "United States"}]:
- Shows only "New Zealand" and "Australia" (filtered by enum)
If enum contained invalid values like ["invalid_1", "invalid_2"]:
- Shows all countries (fallback to full lookup)
This allows conditional allOf blocks to use different lookup URLs per condition while gracefully handling configuration errors.
11.3 Basic Lookup
{
"country": {
"type": "string",
"lookup": "https://api.rayt.io/lookups/countries.json"
}
}
11.4 Lookup JSON Format
[
{ "key": "NZ", "value": "New Zealand" },
{ "key": "AU", "value": "Australia" }
]
11.5 Extended Lookup Properties
[
{
"key": "full_access",
"value": "Full Account Access",
"description": "Access all account features",
"image_src": "https://...",
"requires_2FA": true
}
]
11.6 Cascading Lookups
For hierarchical data:
{
"category": {
"type": "string",
"tags": ["display:cascade"],
"lookup": "https://api.rayt.io/lookups/categories.json"
}
}
Lookup format:
[
{
"key": "electronics",
"value": "Electronics",
"children": [
{ "key": "phones", "value": "Phones" },
{ "key": "laptops", "value": "Laptops" }
]
}
]
11.7 Lookup Expiry and Auto-Refresh
For time-sensitive data (like real-time pricing, stock availability, or session-based options), use the action:timeout:N tag to automatically refresh lookup data after a specified number of seconds.
Basic Usage
{
"account_balance": {
"type": "string",
"title": "Select Account",
"lookup": "https://api.example.com/accounts.json",
"tags": ["action:timeout:60"]
}
}
This will:
- Fetch lookup data initially when the form loads
- Display a countdown indicator inside the select field
- Automatically refresh the lookup data after 60 seconds
- Disable the select field during refresh to prevent stale data selection
Visual Behavior
Normal state: A circular countdown indicator appears inside the select field (to the left of the dropdown arrow). Hovering shows a tooltip with time remaining (e.g., "Refreshes in 45s" or "Refreshes in 1:30").
During refresh: The select is disabled and shows a spinning indicator with an overlay. Users cannot interact with the field until fresh data loads.
Normal state:
┌─────────────────────────────────────┐
│ Select an account ◔ ▼ │ ← countdown circle indicator
└─────────────────────────────────────┘
During refresh:
┌─────────────────────────────────────┐
│ ◌ Loading... 🔄 ▼ │ ← spinning indicator, field disabled
└─────────────────────────────────────┘
Tag Format
| Tag | Description |
|---|---|
action:timeout:30 | Refresh lookup every 30 seconds |
action:timeout:60 | Refresh lookup every 60 seconds |
action:timeout:300 | Refresh lookup every 5 minutes (300s) |
Notes:
- The timeout value is in seconds
- Countdown restarts after each successful refresh
- If the lookup fetch fails, the existing options remain available
- The countdown indicator cycles through 60-second segments (e.g., 90 seconds shows as 1:30, cycling the indicator every minute)
Use Cases
| Scenario | Recommended Timeout |
|---|---|
| Real-time pricing/stock | 30-60 seconds |
| Session-based data | 60-120 seconds |
| Infrequently changing data | 300+ seconds |
Example: Bank Account Selection
{
"title": "Payment Details",
"properties": {
"source_account": {
"type": "string",
"title": "Pay From Account",
"description": "Select the account to pay from (balances refresh automatically)",
"lookup": "https://api.bank.example.com/accounts.json",
"tags": ["action:timeout:60"]
}
},
"required": ["source_account"]
}
The lookup JSON might return:
[
{ "key": "acc_001", "value": "Everyday Account - $1,234.56" },
{ "key": "acc_002", "value": "Savings Account - $5,678.90" }
]
With action:timeout:60, the account balances will refresh every minute, ensuring users see up-to-date information.
11.8 Dynamic Lookup URLs with Variables
Lookup URLs can include dynamic variables that are substituted at runtime based on form field values or application context. This enables filtering lookup options based on user selections.
Variable Syntax
| Pattern | Source | Example |
|---|---|---|
{field_name} | Form field value | {country} |
{field.nested} | Nested object property | {address.city} |
{field[0]} | Array element | {selected_items[0]} |
{$context.name} | Application context | {$context.organizationId} |
Available Context Values
| Context Variable | Description |
|---|---|
{$context.organizationId} | Current organization ID |
{$context.userId} | Current user ID |
{$context.tenantId} | Current tenant ID |
{$context.interfaceId} | Current interface/wizard ID |
Basic Example: Filter by Parent Field
{
"properties": {
"country": {
"type": "string",
"title": "Country",
"lookup": "https://api.example.com/countries.json"
},
"city": {
"type": "string",
"title": "City",
"lookup": "https://api.example.com/cities.json?country={country}"
}
}
}
When the user selects "NZ" for country, the city lookup URL becomes:
https://api.example.com/cities.json?country=NZ
Example: Filter by Organization Context
{
"schema_list": {
"type": "string",
"title": "Select Schema",
"lookup": "https://api.example.com/schemas?org={$context.organizationId}&aa_id={aa_id}"
}
}
This URL substitutes both a context value (organizationId) and a form field value (aa_id).
Behavior
- Automatic refetch: When a referenced field value changes, the lookup is automatically refetched with the new URL
- Cache invalidation: Each unique resolved URL is cached separately
- Error handling: If a variable cannot be resolved (field not found or empty), an error message is displayed instead of the dropdown
Error States
| Scenario | Behavior |
|---|---|
| Variable field not found | Shows error: "Unable to load options: field 'X' not found" |
Variable resolves to null | Shows error (variable must have a value) |
| Variable resolves to empty string | Allowed (substitutes empty string) |
| Variable resolves to array | Joined with comma: ["a","b"] → "a,b" |
| Variable resolves to object | JSON stringified |
Array Value Handling
When a variable references an array field, values are joined with commas:
{
"selected_tags": {
"type": "array",
"items": { "type": "string" }
},
"filtered_items": {
"type": "string",
"lookup": "https://api.example.com/items?tags={selected_tags}"
}
}
If selected_tags is ["red", "blue"], the URL becomes:
https://api.example.com/items?tags=red,blue
11.9 Client-Side Field Filtering
Use the $field: prefix to create a dependent field whose options are filtered based on another field's selected values. This is useful for "select primary from selected items" patterns.
Syntax
{
"dependent_field": {
"type": "string",
"lookup": "$field:source_field_name"
}
}
Example: Select Primary from Multi-Select
{
"properties": {
"party_uses": {
"type": "array",
"title": "Party Uses",
"items": { "type": "string" },
"lookup": "https://api.example.com/party-uses.json"
},
"party_use_primary": {
"type": "string",
"title": "Primary Party Use",
"lookup": "$field:party_uses"
}
}
}
Behavior:
- User selects multiple values in
party_uses(e.g., "Residential", "Commercial") party_use_primarydropdown shows only those selected values- User can pick one as the "primary" option
Conditional Display
It's recommended to hide the dependent field until the source field has selections:
{
"party_use_primary": {
"type": "string",
"title": "Primary Party Use",
"lookup": "$field:party_uses",
"if": {
"party_uses": { "not": { "const": [] } }
}
}
}
Placeholder When No Selection
If the source field has no selections, the dependent field displays a placeholder message:
"Select party_uses first"
Automatic Value Clearing
When the source field changes:
- The dependent field's options are updated
- If the current value is no longer valid (not in new options), it is automatically cleared
- If the current value is still valid, it is preserved
Error States
| Scenario | Behavior |
|---|---|
| Source field has no selections | Shows placeholder: "Select {source_field} first" |
| Source field has no lookup | Shows error: "Source field 'X' has no lookup defined" |
| Selected value becomes invalid | Value is automatically cleared |
Use Cases
| Pattern | Example |
|---|---|
| Primary from multi-select | Select primary contact from selected contacts |
| Default from options | Select default address from selected addresses |
| Lead item from selection | Select lead party use from party uses |
11.10 Lookup Response Transformation
When the API returns data in a format different from the standard { key, value }[] lookup format, use lookupTransform to transform the response at runtime.
Standard Lookup Format
The lookup system expects responses in this format:
[
{ "key": "option1", "value": "Option 1" },
{ "key": "option2", "value": "Option 2" }
]
When to Use lookupTransform
Use lookupTransform when:
- Querying PostgREST endpoints that return nested data
- The API returns arrays inside objects
- Keys and display values need to be extracted from object properties
Simple Path Extraction
Extract an array from a nested path, using each value as both key and value:
{
"scopes": {
"type": "string",
"title": "Scopes",
"lookup": "{API_BASE_URL}/db/v1/dsm_access_applications?select=scopes&id=eq.{p_aa_id}",
"lookupTransform": "[0].scopes"
}
}
API Response:
[{ "scopes": ["ss_File", "ss_Signature", "ss_Email_Address"] }]
Transformed Result:
[
{ "key": "ss_File", "value": "ss_File" },
{ "key": "ss_Signature", "value": "ss_Signature" },
{ "key": "ss_Email_Address", "value": "ss_Email_Address" }
]
Object Config with Key/Value Mapping
For more control, specify which properties to use as key and value:
{
"category": {
"type": "string",
"title": "Category",
"lookup": "{API_BASE_URL}/db/v1/categories?select=items&parent_id=eq.{parent_id}",
"lookupTransform": {
"path": "[0].items",
"key": "id",
"value": "name"
}
}
}
API Response:
[
{
"items": [
{ "id": "cat_1", "name": "Electronics" },
{ "id": "cat_2", "name": "Furniture" }
]
}
]
Transformed Result:
[
{ "key": "cat_1", "value": "Electronics" },
{ "key": "cat_2", "value": "Furniture" }
]
Path Syntax
The path supports both dot notation and array index notation:
| Path | Description |
|---|---|
[0].scopes | First array element, then scopes property |
data.items | Nested object properties |
results[1].data.list | Mixed array indices and properties |
Configuration Options
| Property | Type | Required | Description |
|---|---|---|---|
path | string | Yes | JSON path to extract the array |
key | string | No | Property name to use as lookup key |
value | string | No | Property name to use as display value |
Behavior:
- If only
pathis specified: Each array item becomes both key and value - If only
keyis specified: Uses that property for both key and value - If only
valueis specified: Uses that property for both key and value - If both
keyandvalueare specified: Uses respective properties
Use with Dynamic URLs
lookupTransform works seamlessly with dynamic URL variables:
{
"available_scopes": {
"type": "string",
"title": "Available Scopes",
"lookup": "{API_BASE_URL}/db/v1/dsm_access_applications?select=scopes&id=eq.{p_aa_id}",
"lookupTransform": "[0].scopes"
}
}
When p_aa_id changes, the lookup is refetched and the transformation is applied to the new response.
Error Handling
| Scenario | Behavior |
|---|---|
| Path doesn't exist in response | Returns empty options [] |
| Path doesn't resolve to an array | Returns empty options [] |
| Missing key/value properties | Uses empty string "" |
12. Internationalization (i18n)
12.1 Schema-Level i18n
{
"i18n": {
"en": {
"$schema": {
"title": "Person",
"title_plural": "People",
"description": "Basic person information"
}
},
"mi": {
"$schema": {
"title": "Tangata",
"title_plural": "Tāngata"
}
}
}
}
12.2 Field-Level i18n
{
"i18n": {
"en": {
"given_name": {
"title": "First Name",
"description": "Your legal first name"
}
}
}
}
12.3 Group-Level i18n
{
"i18n": {
"en": {
"group:date_picker:birth.field": {
"title": "Date of Birth",
"description": "Enter your date of birth"
}
}
}
}
12.4 Loading Message i18n
All available loading state keys:
| Key | Description |
|---|---|
$loading_extract | Extracting data from document |
$loading_verify | Verifying information |
$loading_save | Saving data |
$loading_update | Updating information |
$loading_upload | Uploading file |
$loading_create_sub_obj | Creating sub-object |
$loading_permission | Setting permissions |
$loading_link_to_person | Linking to person |
$loading_delete_pending_ver | Deleting old pending verifications |
$loading_pending_ver_resubmit | Pending verification resubmit |
$loading_long_verification_message | Long verification (title + description) |
{
"i18n": {
"en": {
"$loading_extract": { "title": "Reading your document..." },
"$loading_verify": { "title": "Verifying your identity..." },
"$loading_long_verification_message": {
"title": "Verification in progress",
"description": "This may take a few minutes..."
}
}
}
}
12.5 Selection Flow i18n (QrCodeDynamicSection)
{
"i18n": {
"en": {
"$selection_document_type_header": { "title": "Select Document Type" },
"$selection_document_type_description": {
"title": "Choose the type of ID"
},
"$selection_data_source_header": { "title": "Verification Method" },
"$selection_data_source_description": { "title": "How to verify" },
"$selection_credential_format_header": { "title": "Credential Format" }
}
}
}
13. Wizard Page Configuration
13.1 WizardPage Properties
| Property | Type | Description |
|---|---|---|
name | string | Page title (defaults to schema title) |
schemas | string[] | Schema names for this page |
filter | "oneOf"|"anyOf" | Single or multiple schema selection |
description | string | Fallback description |
description_select | string | Description when selecting |
description_create | string | Description when creating |
description_update | string | Description when updating |
optional | boolean | Page can be skipped |
multiple | boolean | Allow multiple selections |
field_list | string[] | Whitelist of fields to show |
optional_fields | string[] | Fields not required |
verify_data | boolean | Verify data (default: true) |
allow_upload | boolean | Allow file upload |
extract_threshold | number | OCR confidence (0-1) |
display_schema_description | boolean | Show schema description |
display_field_title | boolean | Show field labels |
display_field_description | boolean | Show field help text |
display_mode | string | Theme: "light", "dark", "default" |
branching_rule_name | string | Conditional page rule ID |
selection_hierarchy_json | string | URL to hierarchical selection config |
13.2 WizardConfig (Form Link)
| Property | Type | Description |
|---|---|---|
a_id | string | Access Application ID (required) |
pages | array | Page definitions (required) |
review_text | string | Text on review page |
submit_text | string | Submit button label |
expiry_date | number | Days until link expires |
terms_schema | string | Terms wizard schema |
return_to | string | Callback URL |
quick_onboard | boolean | Passwordless signup |
13.3 Hierarchical Schema Selection
When a wizard page has multiple schemas to choose from (using filter: "oneOf"), you can provide a hierarchical navigation structure using selection_hierarchy_json. This is useful when users need to select from many document types organized into categories.
Basic Usage
{
"filter": "oneOf",
"schemas": [
"ss_NZ_DriverLicence",
"ss_AU_NSW_DriverLicence",
"ss_NZ_Passport"
],
"selection_hierarchy_json": "https://api-docs.rayt.io/lookups/raytio_id_document_hierarchy.json"
}
Hierarchy JSON Structure (DeepConfig)
The JSON file defines a recursive tree structure:
interface Option {
option?: string; // ID for i18n lookup
title?: string; // Display title (fallback if no i18n)
description?: string; // Help text (fallback if no i18n)
icon?: string; // URL to icon image
options?: Option[]; // Nested children (for categories)
schema?: SchemaName; // Terminal schema (for leaf nodes)
}
interface DeepConfig {
title?: string;
description?: string;
i18n?: {
[locale: string]: {
[optionId: string]: { title?: string; description?: string };
};
};
options: Option[];
}
Example Hierarchy JSON
{
"title": "Select your document",
"i18n": {
"en": {
"nz": { "title": "New Zealand", "description": "NZ-issued documents" },
"au": { "title": "Australia", "description": "Australian documents" },
"nz_dl": { "title": "Driver Licence" },
"nz_pp": { "title": "Passport" }
},
"mi": {
"nz": { "title": "Aotearoa", "description": "Tuhinga nō Aotearoa" },
"nz_dl": { "title": "Raihana Taraiwa" },
"nz_pp": { "title": "Uruwhenua" }
}
},
"options": [
{
"option": "nz",
"icon": "https://assets.rayt.io/flags/nz.png",
"options": [
{ "option": "nz_dl", "schema": "ss_NZ_DriverLicence" },
{ "option": "nz_pp", "schema": "ss_NZ_Passport" }
]
},
{
"option": "au",
"icon": "https://assets.rayt.io/flags/au.png",
"options": [
{ "title": "NSW Driver Licence", "schema": "ss_AU_NSW_DriverLicence" }
]
}
]
}
In this example:
- NZ options use
optionIDs (nz_dl,nz_pp) that reference i18n translations, enabling multi-language support (English and Māori) - AU options use inline
titledirectly, which is simpler but not translatable
How It Works
- The
SelectSchemaListcomponent fetches the JSON fromselection_hierarchy_jsonURL DeepButtonsrenders the hierarchy as a recursive timeline navigation- Users drill down through categories until they reach a leaf node with a
schemaproperty - The selected schema is then used for form rendering
Notes
- The hierarchy JSON is fetched directly (not through the lookups API)
- i18n is optional; if not provided,
title/descriptionfromOptionare used - Locale matching finds the best available locale based on the user's browser settings
- Icons are optional and displayed as small images on category buttons
14. Common Patterns
14.1 Person with Name Picker
{
"title": "Person",
"properties": {
"given_name": {
"type": "string",
"tags": ["group:name:person"],
"priority": 1
},
"family_name": {
"type": "string",
"tags": ["group:name:person"],
"priority": 2
},
"email": {
"type": "string",
"pattern": "^[^@]+@[^@]+\\.[^@]+$"
}
},
"required": ["given_name", "family_name", "email"]
}
14.2 Address with Google Places
{
"title": "Address",
"properties": {
"route": { "type": "string", "tags": ["group:address"], "priority": 1 },
"locality": { "type": "string", "tags": ["group:address"], "priority": 2 },
"country": { "type": "string", "tags": ["group:address"], "priority": 3 },
"postal_code": {
"type": "string",
"tags": ["group:address"],
"priority": 4
}
},
"required": ["locality", "country"]
}
14.3 Document with Verification
{
"title": "Driver License",
"tags": ["action:verify", "default_camera:rear"],
"properties": {
"front_image": {
"type": "string",
"contentMediaType": "image/jpeg",
"contentEncoding": "base64",
"tags": ["group:images"],
"image_silhouette": "https://assets.rayt.io/silhouettes/nz-license-front.png"
},
"license_number": {
"type": "string",
"tags": ["group:images"],
"readOnly": true
}
},
"verified_fields": ["license_number"]
}
14.4 Conditional Fields
{
"title": "Identity Document",
"tags": ["action:verify"],
"properties": {
"document_type": {
"type": "string",
"enum": ["P", "D"]
},
"document_number": {
"type": "string"
}
},
"allOf": [
{
"if": { "properties": { "document_type": { "enum": ["P"] } } },
"then": {
"properties": {
"document_number": {
"pattern": "^[A-Z]{2}\\d{6}$",
"patternMessage": "Passport: 2 letters + 6 digits"
}
},
"verified_fields": ["document_number"]
}
}
],
"required": ["document_type", "document_number"]
}