Track customers

How to map a business's existing system onto Goomzo's customers, details, activity and segments, and keep them in step.

Read Goomzo for AI agents first: it has the rules every call follows (authentication, phones, idempotency, errors). This page is about modelling: what in the business's system becomes what in Goomzo, and the calls that keep the two in step.

Map the system onto Goomzo

Before writing code, read the existing system and decide, for each thing it knows about a person, which of these it is. Getting this right is most of the work.

In the existing systemIn GoomzoWhy
A person who bought, booked, signed up or was entered by staffCustomerAnyone the business may want to reach or understand
Their phone and emailphone, emailHow Goomzo tells customers apart and reaches them
A fact that has one current value: plan, city, loyalty tier, date of birth, preferred branchDetail in attributesReplaced when it changes; filterable and usable in segment rules
Something that happened: an order, a payment, a visit, a booking, a review, a loginActivityKept as history with its time; counted and added up by segment rules
A label staff give people by hand: "VIP", "Wholesale", "Staff"Segment picked by handMembership the business decides
A group defined by behaviour or facts: "bought twice this month", "gold plan", "inactive 90 days"Automatic segment with rulesGoomzo works out who is in it; nothing to sync

Rules of thumb:

  • Fact or event? If the system would overwrite it, it's a detail. If it would append it, it's activity. An order is activity (purchased); "lifetime orders: 12" is a number Goomzo can work out from activity, so don't also store it as a detail unless the system already has it and it's authoritative.
  • Don't encode groups as details when an automatic segment can say it: record purchased activity and let a segment with "Purchased at least 2 times in the last 30 days" find the regulars.
  • Don't send what the business shouldn't keep: no passwords, card numbers, national ID numbers or health data.

Customers

A customer has a name (required, up to 200 characters) and at least one of phone or email.

  • Identity. Goomzo finds an existing customer by phone first, then email. Two customers can't share a phone or an email. Goomzo ids look like cus_4hq2v9n1c8x0t6pk; store the one you get back next to your own customer record if you'll update or delete by id later.
  • Phones must be mobile numbers in the business's country, with the country code (+255712345678). If the system stores local numbers (0712 345 678), convert them: drop the leading 0, add +255. A landline or a foreign number is refused with a problem on phone; send that customer with only their email, or skip them.
  • Emails are stored in lower case.
  • Details (attributes) are up to 50 named values. Names are lower case letters, numbers and _, starting with a letter, up to 40 characters: loyalty_tier, branch, date_of_birth. Values are text (up to 500 characters), a number, true/false, or null. Use numbers for numbers (25000, not "25,000") so segment rules can compare them, and ISO dates ("1990-05-12") for dates.

Add or update customers: the upsert

The one call most integrations need is POST /v1/customers with ifExists: "update". It adds new people and updates ones Goomzo already has, so you never need to look someone up first.

const { items } = await goomzo.customers.create(
  {
    ifExists: 'update',
    ifSegmentMissing: 'create',
    customers: [
      {
        name: 'Amina Juma',
        phone: '+255712345678',
        email: 'amina@example.com',
        attributes: { loyalty_tier: 'gold', branch: 'Kariakoo' },
        segments: [{ name: 'Online shoppers' }],
        activities: [{ key: 'signed_up', value: 'website', occurredAt: '2026-11-09T08:30:00Z' }],
      },
    ],
  },
  { idempotencyKey: `customer-${localId}-${updatedAt}` },
)
items[0].result // 'created' | 'updated' | 'unchanged'
items[0].customer.id // cus_…

What it does, exactly:

  • Up to 100 customers per call, all or none. One bad phone fails the whole batch, with the error naming it (customers[37].phone). Validate before sending, or fix and resend the whole batch.
  • ifExists: "update" updates the matched customer with what you sent: fields left out stay as they are, and attributes, when sent, replace all of theirs. To change one detail without losing the others, send the full set you know, or leave attributes out. Without it, a phone or email someone already has is an error.
  • segments by id or name adds them to those segments. It never takes them out of others. ifSegmentMissing: "create" makes segments named by name that don't exist yet; names match whatever their case.
  • activities are recorded for the customer in the same call, as below.
  • If a phone belongs to one customer and the email to another, that's an error on email: the system has merged two people Goomzo keeps apart. Send one of them, or fix the data.

Over HTTP it's the same body:

curl https://api.goomzo.africa/v1/customers \
  -H "Authorization: Bearer $GOOMZO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Goomzo-Version: 2026-11-08" \
  -H "Idempotency-Key: customer-1042-2026-11-09T08:30:00Z" \
  -d '{ "ifExists": "update", "customers": [{ "name": "Amina Juma", "phone": "+255712345678" }] }'

Change, read and delete

  • Change one customer by id: PATCH /v1/customers/{id} (goomzo.customers.update(id, {...})). Fields left out stay; an empty string clears phone or email; attributes replace theirs.
  • Read: GET /v1/customers/{id}, or list with q (searches name, phone, email and detail values; a local number like 0712 345 finds +255712345…), segment, sort (-createdAt, createdAt, name, -name), and pages of up to 200. Each customer comes with their segments, including automatic ones they match now.
  • Delete when the person is deleted in the source system, or asks to be forgotten: DELETE /v1/customers/{id}, or POST /v1/customers/delete with up to 1,000 ids. Their activity and memberships go with them. This needs the customers.delete permission, which Customer sync doesn't have on purpose; ask for it only when the integration really deletes.
  • Merge when one person is kept as two customers, say one reached by email and one by WhatsApp: POST /v1/customers/{id}/merge with { "customer": "<the other id>" } (goomzo.customers.merge(id, { customer })). The customer in the path stays: they keep every field they have and take the other's where they have none, join the other's segments, and get their messages, activity and conversation. The other customer is deleted; this can't be undone, and it needs customers.delete too.

Activity

Activity is a customer's history. Record it as it happens: it's what makes segments like "regular buyers" or "gone quiet" work.

await goomzo.activity.record(
  {
    phone: '+255712345678', // or email, or customerId: exactly one
    activities: [
      { key: 'purchased', value: 'Shoes' },
      { key: 'spent', value: 45000 },
    ],
  },
  { idempotencyKey: `order-${order.id}-paid` },
)
  • Find the customer by exactly one of customerId, phone or email. No customer with it is 404 not_found and nothing is recorded: add the customer first (or use the upsert above with activities, which does both).
  • Each entry is a key (same rules as detail names) and one value: text up to 500 characters, a number, or true/false. It can't be empty. Up to 100 entries per call, all or none.
  • occurredAt (ISO 8601) is when it happened; leave it out for now. It can't be in the future. Set it when backfilling, so history lands at the right time.
  • Entries are only added. There's no edit or delete; a refund is its own entry (refunded: 45000).

Name activity well

Pick a small, stable vocabulary, past tense, one meaning per key. Put the amount in its own entry when it's money, so rules can add it up. Reuse these where they fit:

KeyValueWhen
signed_upwhere: "website", "app", "shop"The person created an account
purchasedwhat: "Shoes", or an order numberAn order is paid
spentamount as a number: 45000Alongside purchased, in the business's currency
refundedamount as a numberMoney went back
booked_appointmentthe service: "Haircut"A booking is made
visited_storethe branch: "Kariakoo"A check-in or visit
viewed_offerthe offer: "Weekend 20% off"They opened or claimed an offer
redeemed_offerthe offerThey used it
left_reviewthe rating as a number: 5A review was posted

Don't put ids that are unique per event in the key (purchased_order_1042): that makes thousands of keys and nothing groups. The key says what happened; the value says which.

Segments

A segment is a named group: up to 500 per business, with a name (up to 50 characters, unique ignoring case), a color (green, blue, orange, red, gray) and a description (up to 200 characters). There are two kinds, fixed when the segment is made (kind in the answer).

Picked by hand (kind: "manual")

For labels the business decides. A customer is in up to 20 of them.

  • Add people in the upsert (segments: [{ name: 'VIP' }]), or POST /v1/segments/{id}/customers with up to 1,000 ids, and take them out with POST /v1/segments/{id}/customers/remove.
  • To make a customer's hand-picked segments match the source system exactly, use PUT /v1/customers/{id}/segments with { "names": [...] }: they join the ones named (missing ones are made) and leave the others.

Automatic (kind: "rules")

For groups defined by facts or behaviour. You give rules; Goomzo works out who matches every time the segment is used: counted, listed, filtered on, or sent a broadcast. Prefer these over syncing group membership yourself: you only keep details and activity current, and the groups follow.

import type { SegmentRules } from '@goomzo/sdk'

const rules: SegmentRules = {
  match: 'all', // or 'any'
  conditions: [
    { type: 'activity', key: 'purchased', op: 'atLeast', count: 2, withinDays: 30 },
    { type: 'contact', op: 'hasPhone' },
  ],
}

const { count, customers } = await goomzo.segments.preview(rules) // who matches now, before saving
await goomzo.segments.create({ name: 'Regular buyers', color: 'green', rules })
typeMatches customers whoFields
detailHave a detail that is, isn't, contains, is more or less than a value; or has one or nonekey; op: is, isNot, contains, moreThan, lessThan, isSet, isNotSet; value
activityDid something at least count times (1 if left out), or neverkey; op: atLeast, none; count; withinDays optional
activityTotalHave an activity's number values adding up to more or less than valuekey; op: moreThan, lessThan; value; withinDays optional
anyActivityDid anything, or nothing, in the last withinDays daysop: some, none; withinDays
addedBecame a customer in the last withinDays dayswithinDays
segmentAre or aren't in a segment picked by handop: in, notIn; segmentId
contactHave a phone, or an emailop: hasPhone, hasEmail
  • Up to 10 conditions; withinDays is 1 to 3,650. is/isNot compare exactly (text, number or true/false); contains is text, ignoring case; moreThan/lessThan only match number values.
  • Up to 50 automatic segments per business. Rules can only use segments picked by hand, and such a segment can't be deleted while rules use it (409 segment_in_use).
  • No one is added or removed by hand. Adding to one is 409 segment_fills_itself; naming one in the upsert's segments is an error on that field; PUT /v1/customers/{id}/segments ignores them.
  • Change the rules with PATCH /v1/segments/{id} and new rules. A segment picked by hand can't be given rules: make a new one.
  • Rules that take longer than 5 seconds to work out are refused (422 rules_too_slow); use fewer conditions.

Useful automatic segments to suggest:

SegmentRules
New this weekadded, withinDays: 7
Regular buyersactivity purchased atLeast count: 2, withinDays: 30
Big spendersactivityTotal spent moreThan value: 500000, withinDays: 90
Gone quietanyActivity none, withinDays: 90
Gold membersdetail loyalty_tier is "gold"
Reachable by SMScontact hasPhone

Recipes

Backfill: bring existing customers in

  1. Read the source in pages. Map each person to { name, phone, email, attributes }, normalizing phones to +255…. Skip people with neither a valid phone nor an email, and log them.
  2. Send batches of up to 100 to POST /v1/customers with ifExists: "update", an idempotency key per batch (backfill-<run id>-<batch number>), and their past events as activities with occurredAt.
  3. On a 400, read fields, drop or fix the named customers, and resend that batch. On 429, wait Retry-After.
  4. Store each returned customer.id against the source record.

Running it again is safe: matched customers come back unchanged. For a one-off spreadsheet the business has, POST /v1/customers/import takes up to 5,000 rows, accepts local phone numbers, skips bad rows instead of failing, and has dryRun. Prefer the upsert for code.

Keep in step as things happen

Hook the source system's own events, and call Goomzo from its server, after its own write succeeds:

Source eventCall
Sign-up, profile change, staff editUpsert the customer (ifExists: "update")
Order paidPOST /v1/activity with purchased and spent, key order-<id>-paid
Booking, visit, reviewPOST /v1/activity with its entry
Tag added or removed by staffPOST /v1/segments/{id}/customers or /remove, or PUT /v1/customers/{id}/segments
Person deletedDELETE /v1/customers/{id}

Don't make the user wait on Goomzo: send from a background job or queue, retry on 429 and 5xx with the same idempotency key, and alert on anything else. Because every call is idempotent and the upsert matches by phone and email, replaying a queue is harmless.

Hear back from Goomzo

Register a webhook for customer.created to learn about customers added in Goomzo itself (by staff, or an import), for example to create them in the source system. Check every delivery's signature (verifyWebhook in the SDK) and use the event id to skip repeats.

Before you finish

  • The API key is on the server only, with Customer sync permissions (plus customers.delete only if needed).
  • Every request sends Goomzo-Version, and every POST an Idempotency-Key from the source's own ids.
  • Phones are converted to +255…; people without a usable phone or email are skipped and logged.
  • Facts are details, events are activity, and groups are automatic segments wherever rules can say them.
  • Batches are at most 100, and a 400 is fixed from fields rather than retried.
  • 429 and 5xx are retried with backoff; nothing blocks the source system's own users.
  • Activity keys are a small, past-tense vocabulary; money is a number in its own entry.

Last updated