Please make sure you read the Conventions before continuing with this guide.
Prerequisites
You will need an X-API-KEY and a Basic Access Authentication token generated from your username and password.
How to know if I can use a dynamic form
When working on the sales flow, the product that you select may contain a property named dynamicFormId if the property exists and contains a value other than null, it means that the product can use a dynamic form to request person information.
Why should I use a dynamic form?
Dynamic form fields that are captured can be associated with key(s) in the peopleLookup collection. This will allow you to augment the amount of information you associate with each passenger (or sender/receiver when purchasing a parcel), and enable easy pre-population of form data based on previous interactions.
You can get a render-ready dynamic form context using the /accounts/renderable-dynamic-forms/:dynamicFormId endpoint with the dynamicFormId of the product. The product can be retrieved from /inventory/products/:productId.
This endpoint returns:
dynamicForm: Original dynamic form record (mapping and metadata).captureFields: Enriched field definition ready to render (resolved enum options, date formats, defaults).dynamicFormsGrouppedFields: Field keys grouped by UI rows (max 2 inputs per row).
Optional query string parameters:
providerId: Provider context (ObjectId).currentLanguage: Language used for translated collection values (for exampleen,es,fr).documentTypeIds: Comma-separated document type ids to filter document type options.
How do I use the information contained in the dynamic form object to build my own forms to be compatible with the Betterez sales flow?
We have identified certain fields that are required during our sales process, or for the purpose of lookups: documentType, documentNumber,firstName, lastName, email, phone, and fullName. This is defined "mappedStandardBtrzFields", and you can map multiple dynamic form fields to a single standard field. The exception being documentType and documentNumber. These fields are reserved for unique data you would like to capture and search on. For example a document type might be "Passport", and the documentNumber might be a unique number from a person's passport. These should be defined using /inventory/document-types. Any validation added to the document type should be checked on the client side to reduce failures.
The mappingSeparators object should be respected for any fields in mappedStandardBtrzFields that are associated with multiple dynamic fields.
If, peopleLookupEnabled is true, you can check the peopleLookupFields object to see which fields should be used for the lookup. These fields should be required, and be as unique as possible.
Finally, use captureFields.properties and dynamicFormsGrouppedFields to construct the form. The dynamicForm.definition object is still useful for validation metadata (for example required) and standard-field mapping metadata.
Client-side example (consume and render)
The example below shows a browser-side implementation that:
- Calls the new renderable endpoint.
- Builds a simple HTML form from the response.
- Produces both raw dynamic form values and standard Betterez mapped values for cart payload usage.
const API_BASE = "https://sandbox-api.betterez.com";
async function getRenderableDynamicForm({
dynamicFormId,
jwtToken,
apiKey,
providerId,
currentLanguage = "en",
documentTypeIds = []
}) {
const qs = new URLSearchParams();
if (providerId) qs.set("providerId", providerId);
if (currentLanguage) qs.set("currentLanguage", currentLanguage);
if (documentTypeIds.length) qs.set("documentTypeIds", documentTypeIds.join(","));
const response = await fetch(
`${API_BASE}/accounts/renderable-dynamic-forms/${dynamicFormId}?${qs.toString()}`,
{
method: "GET",
headers: {
authorization: `Bearer ${jwtToken}`,
"x-api-key": apiKey,
accept: "application/json"
}
}
);
if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new Error(body.message || `Failed to load dynamic form (${response.status})`);
}
return response.json();
}
function renderDynamicForm(container, renderableResponse) {
const {captureFields, dynamicFormsGrouppedFields} = renderableResponse;
const properties = captureFields?.properties || {};
const form = document.createElement("form");
form.id = "passenger-dynamic-form";
dynamicFormsGrouppedFields.forEach((rowKeys) => {
const row = document.createElement("div");
row.className = "df-row";
rowKeys.forEach((key) => {
const field = properties[key];
if (!field) return;
const wrapper = document.createElement("div");
wrapper.className = "df-field";
const label = document.createElement("label");
label.htmlFor = key;
label.textContent = key;
wrapper.appendChild(label);
let input;
if (Array.isArray(field.enum)) {
input = document.createElement("select");
field.enum.forEach((option) => {
const opt = document.createElement("option");
opt.value = option.key;
opt.textContent = option.value;
input.appendChild(opt);
});
} else if (field.format === "date-time") {
input = document.createElement("input");
input.type = "date";
} else {
input = document.createElement("input");
input.type = field.type === "number" ? "number" : "text";
}
input.id = key;
input.name = key;
if (field.default != null) input.value = field.default;
wrapper.appendChild(input);
row.appendChild(wrapper);
});
form.appendChild(row);
});
container.replaceChildren(form);
return form;
}
function buildMappedStandardFields(dynamicForm, dynamicValues) {
const mapped = dynamicForm.mappedStandardBtrzFields || {};
const separators = dynamicForm.mappingSeparators || {};
function getMappedValue(fieldName) {
const rules = mapped[fieldName];
if (!rules) return "";
const asArray = Array.isArray(rules) ? rules : [rules];
const chunks = asArray
.map((rule) => dynamicValues[rule.key])
.filter((value) => value != null && value !== "");
return chunks.join(separators[fieldName] || " ").trim();
}
return {
documentTypeId: getMappedValue("documentType"),
documentNumber: getMappedValue("documentNumber"),
firstName: getMappedValue("firstName"),
lastName: getMappedValue("lastName"),
fullName: getMappedValue("fullName"),
email: getMappedValue("email"),
phone: getMappedValue("phone")
};
}
With this approach, your UI should keep two data sets:
- Dynamic values keyed by dynamic form field name (for people lookup and auditing).
- Mapped standard values (
firstName,lastName,documentTypeId,documentNumber, etc.) for sales/cart payload compatibility.
Definition example
Click to expand
{
"dynamicforms": {
"_id": "63e9d9c9f94f8d072b35ezz2",
"accountId": "yourAccountId",
"name": "PassengerCapture",
"mappedStandardBtrzFields": {
"documentType": {
"key": "documentType",
"value": "documentType"
},
"documentNumber": {
"key": "documentNumber",
"value": "documentNumber"
},
"email": [
{
"key": "customEmail",
"value": "customEmail"
}
],
"firstName": [
{
"key": "customFirstName",
"value": "customFirstName"
},
{
"key": "customSecondFirstName",
"value": "customSecondFirstName"
}
],
"lastName": [
{
"key": "customlastName",
"value": "customlastName"
},
{
"key": "customSecondLastName",
"value": "customSecondLastName"
}
],
"fullName": [
{
"key": "customFirstName",
"value": "customFirstName"
},
{
"key": "customLastName",
"value": "customLastName"
}
],
"phone": [
{
"key": "customPhone",
"value": "customPhone"
}
]
},
"mappingSeparators": {
"documentType": " ",
"documentNumber": " ",
"email": " ",
"firstName": " ",
"lastName": " ",
"fullName": " ",
"phone": " "
},
"peopleLookupEnabled": true,
"peopleLookupFields": {
"documentType": true,
"documentNumber": true,
"email": false,
"firstName": false,
"lastName": false,
"fullName": false,
"phone": false
},
"peopleLookupTrigger": "documentNumber",
"definition": {
"id": "passengerFields",
"type": "object",
"required": [
"documentType",
"documentNumber",
"customFirstName",
"customLastName"
],
"properties": {
"documentType": {
"type": "string",
"x-btrz-data": {
"collection": "document_types",
"key": "_id",
"value": "lexiconKeys.name",
"ord": "ord",
"default": ""
},
"x-btrz-row-index": 0
},
"dateOfBirth": {
"type": "string",
"format": "date-time",
"x-btrz-date": {
"from": "new Date()"
},
"x-btrz-row-index": 3
},
"documentNumber": {
"type": "string",
"x-btrz-row-index": 0
},
"customFirstName": {
"type": "string"
},
"customSecondFirstName": {
"type": "string"
},
"customLastName": {
"type": "string"
},
"customSecondLastName": {
"type": "string"
},
"gender": {
"type": "string",
"enum": [
"M",
"F"
]
},
"customPhone": {
"type": "string"
},
"customEmail": {
"type": "string",
"x-btrz-row-index": 3
}
}
},
"createdAt": {
"value": "2023-05-25T19:44:32.846Z",
"offset": 0
},
"updatedAt": {
"value": "2023-06-12T23:57:21.364Z",
"offset": 0
}
}
}
Decoding Dynamic Form JSON Fields
Definition
properties
This section encapsulates the definition of custom fields. Each key corresponds to a unique custom field.
required
Specify the mandatory custom fields for this form. The user interface (UI) should validate and ensure their completion.
MappedStandarBtrzFields
This property facilitates the mapping between custom and standard Betterez fields.
The keys represent custom fields, and their corresponding values denote the standard Betterez fields they align with. This mapping is crucial for seamless integration into the sales flow.
MappingSeparators
This property defines the separators to be used in aggregated fields. For instance, in the "fullName" field, which may consist of "firstName" and "lastName," the specified separator (comma, space, etc.) should be used.
MappedProductBtrzFields
This property illustrates the mapping to more specific fields for different types of products.
For example, in the case of a "parcel," it provides a mapping for fields like "address," "city," "country," "province," and "zipCode."
However, for reservations, focusing on the fields in MappedStandardBtrzFields is sufficient.
MappingProductSeparators
This property define the separators to be used in aggregated fields specific to certain product types, similar to MappingSeparators but tailored for product-specific fields.
There are some properties that require special attention to define the UI behaviour:
peopleLookupEnabled
If enabled, the form should work with people lookup endpoints. It's important to note that at the API level, this field is not validated. Nevertheless, it serves as a useful indicator to determine whether the form will interact with lookup endpoints or not.
peopleLookupFields
This property defines which fields are used to look up a person when people lookup is active. The UI should prevent querying for a person if any of the specified fields in the GET /people-lookups request is missing.
peopleLookupTrigger
This property defines the field used to trigger the GET /people-lookups request.
availableForParcel
If set to true, this dynamic form will be available exclusively for parcels. When set to false, the form is valid for reservation products. This setting makes certain standard fields mandatory in the parcel sales flow.
captureAddressInfo
This property is only applicable when working with parcels. It enables the mapping of custom fields to specific standard parcel Betterez fields.
This mechanism is similar to the mapping process with standard Betterez fields. However, in this case, it is specifically applied to associating custom fields with standard Betterez fields for the "parcel" product type.