Template-variable catalogue — what the picker consumes
When you build a campaign template composer, use the catalogue endpoint as the source for the merge-tag picker. It returns the fields available to the caller’s tenant with the label, group, and type the picker needs. Insert the row’skey between double braces, not its display name.
What the endpoint is
CallGET /api/v1/campaigns/template-variables with an authenticated API key. The response is a standard Orbit envelope whose data array contains rows shaped like this:
standard— the five resolver-backed fields:first_name,last_name,phone,email, andcompany.contact— non-standard fields from the contact record that the caller can use when composing a message.custom— custom-field definitions owned by the caller’s tenant.
key as the immutable insertion value. For example, a picker row with key: "first_name" inserts {{first_name}}; it must not insert {{First name}}.
Why a catalogue exists
A picker should not offer a tag that the delivery resolver will remove. The standard group is derived from the resolver’sTEMPLATE_VARIABLES in template-interpolate.ts, so the catalogue and the delivery path share one source of truth. If a resolver field is added without a matching friendly label, the catalogue uses a humanized fallback rather than silently dropping the row.
This makes drift visible: the catalogue tests compare its standard rows with the resolver’s variables. A change to the resolver therefore fails loudly during validation instead of leaving a picker with a missing or undeliverable merge tag.
Group semantics
Standard fields
Standard fields are the safe, resolver-backed subset. The API usesSTANDARD_DISPLAY_NAMES for labels such as First name, Last name, and Company. The humanizeFieldName fallback turns an unlisted key such as customer_tier into Customer tier while keeping the key unchanged.
The display label is for the UI only. Always insert {{key}} in the template body.
Contact fields
Thecontact group contains non-standard contact fields available to the authenticated caller. Keep these separate from standard fields in the picker so authors can see which values come from the broader contact record rather than the resolver’s guaranteed standard set.
Custom fields
Thecustom group is tenant-scoped. The endpoint derives it from the custom-field definitions belonging to the caller’s tenant, so one tenant’s definitions are never exposed to another tenant. The read is performed through the tenant-safe query path with transient database-read retry (listCustomFieldDefinitions via safeTenantQuery and retryDbReadOnTransient). Do not cache these rows across tenants; refresh them when the active tenant changes.
Sample request and response
Send the request with the same API key and tenant context used by the campaign composer:data as a flat list. Group and sort it in the UI, and preserve the API’s keys when inserting tags. The meta object is an envelope, not another catalogue group.
Best practice: prefer standard fields
Treatstandard as the safe subset for reusable campaign content. Prefer {{first_name}}, {{last_name}}, {{phone}}, {{email}}, and {{company}} unless the message specifically needs a contact or custom field. Standard fields are the ones tied directly to the delivery resolver; contact and custom values depend on the available record and tenant definitions.
For optional personalization, keep a fallback in the surrounding copy and test with contacts that have the field populated and contacts that do not. Never hard-code a display label or assume that a custom field exists in every tenant.
Implementation references
The standard labels and humanized fallback are defined intemplate-variables.ts. The public route and response envelope are defined in template-variables.controller.ts.
Related guides
- Outbound templates — create, approve, and reuse campaign templates.
- Custom fields — manage tenant-owned custom contact fields.