Skip to content

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:

PathTrigger
confirm ingressNormal checkout completion
epm-handoff ingressCustomer picked an external payment method (e.g. PayPal)
webhook / reconcilePush fallback and recovery/resync
address-update ingressCustomer 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

filtrera
{
  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 */ }
}
FieldNotes
cartIdThe cart the address is being applied to.
customerKustom's order-level customer block, verbatim.
billingPartyParty attributes derived from the billing address.
shippingPartyParty 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:

filtrera
let isPerson = input.billingParty.kind match
  'organization' |> false
  |> true

A 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.

FieldRequiredNotes
effectyesMust be 'custom'.
typeyesMust be 'orderField'.
keyyesField name, without a prefix. Lands on the created order under this exact name.
valueyesThe 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

filtrera
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:

filtrera
let staged = order.dynamic->'kustomParty'

let givenName = staged match
  { given: text } |> staged.given
  |> nothing

Parts 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.

See Also

© 2026 Hantera AB. All rights reserved.