Customer templates
A customer template is a provider-scoped, reusable bundle of location template references. It captures the set of location blueprints you would normally roll out for a new customer, so a caller can replay the same location templates at customer-onboarding time instead of re-selecting them by hand.
What a customer template is (and isn't)
- It is a list of references. The
locationTemplateIdslist is held verbatim as a list of UUIDs that point at location templates. Each id is validated at save time. The request is rejected if any entry doesn't match an existing, non-deleted location template. - It is not an active resource. Saving a customer template doesn't create customers or locations. Materialization is the caller's responsibility: read the template, look up each referenced location template, and replay them under the new customer.
- It is scoped to a provider. The
providerIdis set on create and immutable. Authorization gates on the parent provider. Anyone withupdateon the provider can manage its customer templates.
Fields
| Field | Description | Example |
|---|---|---|
id | Unique ID assigned at create time | 3f5a7b9c-1d2e-4f3a-9876-1234567890ab |
providerId | Parent provider — set on create, immutable | 8b14d4f9-3a52-4b91-8e7c-d3e44b5c1f2a |
name | Human-readable name, unique within the provider (1–255 chars). Identifies the template itself; it isn't the created customer's name | Standard customer rollout |
customerName | Default for the created customer's name — seeds the apply body's name. Required (1–255 chars). Supports {{variable}} substitution | {{provider.name}} tenant |
customerDescription | Default for the created customer's description — seeds the apply body's description. Optional (≤1000 chars). Supports {{variable}} substitution | Managed by {{provider.name}} |
locationTemplateIds | List of location template UUIDs the template references. Each id must resolve to a live, non-deleted location template at save time | |
ownerIdentityId | Identity that owns the template | c0ffee00-1111-2222-3333-444455556666 |
createdBy, createdAt, updatedAt, deletedAt, deletedBy | Standard lifecycle fields |
Applying a customer template
Materializing a customer template into real resources is a two-step interaction against a single endpoint pair, mirroring the location template apply flow but one level up: the customer is created first, then each referenced location template is applied under it in order.
The split pays off when the caller is a UI: the top-level _placeholders plus each embedded location's own
_placeholders are enough to render a nested form (customer header, one section per location, indexed connector
rows) without the UI needing to know anything about the underlying schema. CLIs benefit too. connect-cli apply <customerTemplateId> can render the full preview, prompt the operator for each placeholder at every level, and submit the
filled body in one POST.
See the API reference for request/response examples and the full path table.
The preview (GET)
GET /customer-templates/{templateId}/apply returns a CreateCustomerFromTemplate payload shaped exactly like the
POST body, pre-filled from the template:
- The new customer's
name/descriptionare seeded from the template'scustomerName/customerDescription. - The
locationTemplateIdslist is expanded server-side into alocationsarray: oneCreateLocationFromTemplateper referenced location template, in order. Each entry is itself a location template preview (name,description,connectorTemplates, the deprecated inlineconnectors, nested_placeholders) — so a customer template apply carries the whole three-level chain in one body. - A top-level
_placeholderslist describes the caller-supplied customer fields ($.name, required;$.description). - Each embedded location entry carries its own
_placeholdersrooted at that location's payload ($.name,$.description,$.address,$.longitude,$.latitude,$.virtual,$.cloudProvider,$.cloudRegion; see Cloud placement). The$.customerIdplaceholder is deliberately omitted. The server injects the new customer's id during apply, so the caller must not fill it in. - Each placeholder's
valueechoes the underlying template, so the UI can show the original blueprint as defaults the caller can override. The customer'sname/descriptionare seeded from the template'scustomerName/customerDescription, and each location'sname/descriptionfrom its location template'slocationName/locationDescription. Those values are rendered (with{{variable}}substitution) at apply time. The connector fields are provider-authored, so they are echoed but not surfaced as placeholders. For more info, see Variable substitution.
The preview strips fields that don't belong in the caller's submission: id, ownerIdentityId, and the
createdBy/createdAt/updatedAt/deletedAt/deletedBy lifecycle fields. The providerId is always taken from
the customer template, never from the caller. The whole list is READ_ONLY for Jackson, so a caller can echo the
entire response straight back into POST without filtering.
The apply (POST)
The POST body is a CreateCustomerFromTemplate — a customer name/description plus a locations: List<CreateLocationFromTemplate>:
nameanddescriptionare seeded from the template, then rendered. The preview pre-fills them from the template'scustomerName/customerDescription. The caller may keep or override them; any{{variable}}tokens they contain are rendered at apply time (againstprovider.*only; see Variable substitution) to become the new customer's name/description. The rendered name is validated for length and per-provider uniqueness.locationsorder matters. The body'slocationslist MUST have the same length as the customer template'slocationTemplateIds, in the same order. A length mismatch returns400 Bad Requestbefore any resource is created. Each entry is paired by index with its referenced location template, which is what the server uses to apply it.providerIdis server-set. Anything the caller sends for it is overwritten with the customer template's provider id before validation.locations[*].customerIdis server-set. The server creates the new customer first, then injects its id ascustomerIdon every embedded location body before that location is materialized. Anything the caller sends is ignored.- Each embedded location's
virtual/cloudProvider/cloudRegionis chosen per apply, same as a standalone location template apply. These fields aren't stored on the referenced location template; the caller sets them independently for every entry in thelocationslist. locations[*].connectorTemplates[*]must line up with each referenced location template'sconnectorTemplateIds— one entry per reference, in order, exactly as on a standalone location template apply. A mismatched count is rejected before anything is created.locations[*].connectorTemplates[*].locationIdandlocations[*].connectors[*].locationIdare server-set. Same rule as the location template apply: overwritten per connector with the new location's id.- Errors name the entry they came from. A failure inside a nested connector reports its full path —
locations[0].connectorTemplates[1].name— rather than collapsing to the request root, so a caller submitting a whole chain learns exactly which entry it got wrong. - The whole call is one transaction. The customer is inserted, then each location and its connectors are bound, validated, and created in order. Any failure (bean validation, authorization, a Ziti-side error) rolls the whole thing back, including the customer. Ziti compensations registered against the connector creates fire on rollback so the network state stays consistent with the database.
Authorization
The caller needs three things in combination:
- Read on the customer template: Gated through its parent provider.
- Create on customers under the template's provider. This is verified up front, before any resource is touched.
- Create on locations and connectors at each new location's path. This is verified per location and per connector during materialization. A caller who has customer-create but not location- or connector-create will get past the Customer step and then see the whole apply roll back when the first per-location check fails.
Variable substitution
The apply body's name and description (seeded from the template's customerName/customerDescription) support
{{variable}} placeholders, resolved at apply time. Because the customer is the resource being created, the only
namespace in scope is provider.*. There is no customer.* (it would be circular) or location.* (no location
exists yet):
| Property | In-scope variable namespaces |
|---|---|
$.name | provider.id, provider.name, provider.networkId, provider.networkGroupId, provider.organizationId, provider.custom.* |
$.description | provider.id, provider.name, provider.networkId, provider.networkGroupId, provider.organizationId, provider.custom.* |
provider.custom.* covers the provider's user-defined custom variables, e.g.
{{provider.custom.edgeDomain}}.
Substitution is strict: a placeholder that references a variable not in the catalog (a typo, a custom variable not
defined on the provider, or any customer.*/location.* token) fails the whole apply with a 4xx listing the
offending variables, rather than emitting a blank or a literal {{…}}. The embedded location templates render their
own fields (including customer.* and location.*) later, once the customer and each location exist. See the
location template Variable substitution.
The provider's user-defined {{provider.custom.<name>}} variables are in scope here too, alongside the standard provider.* keys; customer.custom.* is not (the customer does
not exist yet). See Custom variables.
Lifecycle hooks
Customer Connect listens for resource-change events from upstream resources and keeps templates in sync:
- Provider deleted: Every customer template under that provider is deleted in turn. The cascade is driven by the
connect-providerDELETEDevent. - location template deleted: The deleted template's id is stripped from each customer template's
locationTemplateIdslist. The customer template itself is preserved, even if itslocationTemplateIdslist ends up empty. The cascade is driven by theconnect-location-templateDELETEDevent.
These hooks are best-effort housekeeping; they keep stored references from pointing at resources that no longer exist. They don't affect any customers or locations that were already materialized from the template.
Filtering
The list endpoint accepts the usual id / name / providerId / ownerIdentityId / createdAt / updatedAt /
deleted filters, plus a locationTemplateId filter that matches customer templates whose stored
locationTemplateIds array contains the given location template id. That filter is implemented as a JSON-aware
predicate against the locationTemplateIds column, so large providers can narrow efficiently without scanning every
customer template in the application tier.