OnKustomCustomerData hook
Kustom Checkout collects more about the customer than a Hantera address can hold: the given/family name split, and an order-level customer block carrying type, gender, date of birth, organization registration id and VAT id.
Hantera addresses carry a single name field. That is deliberate — it maps 1:1 onto company names, it is directly printable, and it means every consumer renders a name the same way. So when the Kustom app maps a Kustom address onto a Hantera address it joins given_name and family_name into that one field, and the rest of the customer block is dropped.
That is the right call for the address. But some systems genuinely need the parts: CRM and marketing platforms model contacts as first/last, and some legacy carrier APIs demand a split. Re-deriving them later by splitting on whitespace is lossy — it mangles van der Berg, Spanish double surnames, and CJK name order.
OnKustomCustomerData hands the original, unjoined data to merchant rules and lets them decide what — if anything — to keep.
Nothing is stored by default
The Kustom app never writes this data on its own. With no listener, the hook returns nothing and no extra fields are written. This keeps personal data collection opt-in per tenant rather than a platform-wide default.
When It Fires
Every time the Kustom app applies a Kustom address to a cart:
| Path | Trigger |
|---|---|
confirm ingress | Normal checkout completion |
epm-handoff ingress | Customer picked an external payment method (e.g. PayPal) |
webhook / reconcile | Push fallback and recovery/resync |
address-update ingress | Customer edits their address in the Kustom widget |
The hook fires from the shared address-mapping helper, so all paths behave identically. The address-update path re-fires as the customer edits, so staged data cannot go stale mid-checkout.
Hook Input
{
hook: 'OnKustomCustomerData'
cartId: text
customer: {
type: 'person' | 'organization'
gender?: 'male' | 'female'
date_of_birth?: text
organization_registration_id?: text
vat_id?: text
}
billingParty: {
given: text | nothing
family: text | nothing
title: text | nothing
organizationName: text | nothing
kind: text | nothing
gender: text | nothing
dateOfBirth: text | nothing
organizationRegistrationId: text | nothing
vatId: text | nothing
}
shippingParty: { /* same shape */ }
}| Field | Notes |
|---|---|
cartId | The cart the address is being applied to. |
customer | Kustom's order-level customer block, verbatim. |
billingParty | Party attributes derived from the billing address. |
shippingParty | Party attributes derived from the shipping address. |
The party record always has the same shape: anything Kustom did not return is nothing rather than absent, so listeners can match on it without guarding for missing keys. A kind of organization means organizationName is set and the name parts describe the contact person rather than the addressee.
Check kind before using the name parts
On a B2B order Kustom sends organization_name and the contact person's given_name / family_name. The Hantera address maps the company to name and the contact person to attention, which is correct — but it means the party's name parts describe a different person than the addressee.
If your consumer treats name parts as the customer's own name — as the CRM app does, recomposing name from them — staging them for an organization would replace the company name with the individual's.
Guard on kind:
let isPerson = input.billingParty.kind match
'organization' |> false
|> trueA company has no given/family name, so staging nothing is the honest answer. The contact person is still preserved on the address's attention field.
Emitting Fields
Listeners emit a custom effect with type = 'orderField'. Each effect stages one dynamic field on the order that the cart will become.
| Field | Required | Notes |
|---|---|---|
effect | yes | Must be 'custom'. |
type | yes | Must be 'orderField'. |
key | yes | Field name, without a prefix. Lands on the created order under this exact name. |
value | yes | The value to store. Any JSON-serializable Filtrera value. |
Return the key unprefixed
The app adds commerce's order: projection prefix itself. Return kustomParty, not order:kustomParty — the latter becomes order:order:kustomParty and lands on the order under the wrong name.
The app owns the prefix deliberately. Only order:, delivery: and field: keys are projected onto the created order; anything else stays app-private cart state and is discarded at completion. Had listeners chosen the prefix, forgetting it would mean silent data loss — the value is written, looks correct on the cart, and vanishes when the order is created, with the symptom appearing in a different app entirely.
It also means everything a listener writes is namespaced under order:, so it cannot reach the cart keys the app owns (address, invoiceRecipient, email, phone).
Name your keys defensively
Order dynamic fields are a namespace shared by every installed app, and keys arrive unprefixed. Use a vendor- or app-qualified name (kustomParty, crm_nameParts) rather than a generic one like party or customer, which another app may also write.
Example: stage the name split for a CRM sync
import 'iterators'
param input: {
hook: 'OnKustomCustomerData'
cartId: text
billingParty: {
kind: text | nothing
given: text | nothing
family: text | nothing
title: text | nothing
}
}
// A company has no given/family name — see the note on `kind` above.
let isPerson = input.billingParty.kind match
'organization' |> false
|> true
from isPerson match
true |> [{
effect = 'custom'
type = 'orderField'
key = 'kustomParty'
value = {
given = input.billingParty.given
family = input.billingParty.family
title = input.billingParty.title
}
}]
|> []Stage only the fields a consumer actually uses, as a single nested record. Keeping them in one record means that when the platform's order-locations model lands, the whole thing relocates onto the location's dynamic data unchanged.
A CRM rule then reads it off the order and copies the parts onto the customer:
let staged = order.dynamic->'kustomParty'
let givenName = staged match
{ given: text } |> staged.given
|> nothingParts are used verbatim — never derived by splitting a joined name. No general splitter is correct across compound surnames or CJK ordering, and for a company the concept does not apply. A customer with no parts simply keeps its name, which is already complete. A consumer that specifically requires a first/last split should derive one itself, using whatever rule suits its target system.