Recipes / Customer sync for marketing

CRM

Customer sync for marketing

Export customers and their segments to a mail or loyalty platform, incrementally and without duplicates.

GET/api/customers GET/api/customers/{id} GET/api/customer-segments

The same flow that feeds our marketing connector.

List the segments and pick your audience

Segments are the audiences your merchant already maintains: wholesale, newsletter, VIP. One call lists them all, alphabetical, with a customer_count so you can size the sync before running it.

curl -s https://api.storekeeper.me/api/customer-segments -H "Authorization: Bearer $TOKEN"
{
  "count": 3,
  "total": 3,
  "data": [
    { "id": 7,  "name": "Groothandel",       "description": "B2B bakkerijklanten", "price_list_id": 12, "customer_segment_type_id": 1, "customer_count": 48 },
    { "id": 11, "name": "Nieuwsbrief",       "description": null,                  "price_list_id": null, "customer_segment_type_id": 1, "customer_count": 1893 },
    { "id": 14, "name": "VIP stamgasten",    "description": "10% vaste korting",   "price_list_id": 19, "customer_segment_type_id": 1, "customer_count": 112 }
  ]
}

The id is what you feed into /api/customers?segment_id= in the next step. price_list_id tells you a segment also binds custom prices; irrelevant for mailing, useful to know it exists.

Drain the segment with pagination

Page through the segment members with start/limit (max 500 per page) until you have total rows. q is a word search over name, email and similar fields; combine it with segment_id to spot-check, or leave it off for the full drain.

# page 1, then start=500, start=1000, ... until start >= total
curl -s "https://api.storekeeper.me/api/customers?segment_id=11&start=0&limit=500" \
  -H "Authorization: Bearer $TOKEN"
{
  "q": "",
  "start": 0,
  "limit": 500,
  "count": 500,
  "total": 1893,
  "data": [
    { "id": 88412, "name": "Anouk Bakker", "own_id": "", "email": "anouk.bakker@example.nl",
      "business_name": "", "contact_person_name": "Anouk Bakker" },
    { "id": 88437, "name": "Modehuis Elegance B.V.", "own_id": "ERP-2041", "email": "inkoop@modehuis-elegance.nl",
      "business_name": "Modehuis Elegance B.V.", "contact_person_name": "S. van Dijk" },
    { "id": 88502, "name": "Kassa klant", "own_id": "", "email": "",
      "business_name": "", "contact_person_name": "" }
  ]
}

The row id is the customer's relation_data_id, the same id orders carry. That makes it the join key for everything: you can attach purchase history to a mail profile by matching an order's relation_data_id to this id.

Gotcha: that third row is real life. POS checkouts create anonymized guest customers with empty names and empty or placeholder emails. They are valid customers in Storekeeper, but useless in a mail platform. Filter them out (next steps), do not "fix" them.

Fetch full detail where you need it

The list rows carry enough for most mail syncs. When you also want phone and address (postal campaigns, country-based segmentation), fetch the detail per customer. Only do this for the rows that survive your email filter; it is one call per customer.

curl -s https://api.storekeeper.me/api/customers/88437 -H "Authorization: Bearer $TOKEN"
{
  "id": 88437,
  "name": "Modehuis Elegance B.V.",
  "own_id": "ERP-2041",
  "email": "inkoop@modehuis-elegance.nl",
  "business_name": "Modehuis Elegance B.V.",
  "contact_person_name": "S. van Dijk",
  "phone": "+31612345678",
  "address": {
    "street": "Herenstraat",
    "streetnumber": "24",
    "zipcode": "3512 KB",
    "city": "Utrecht",
    "country_iso2": "NL"
  }
}

Push to your marketing platform, idempotently

Three rules keep the sync clean across re-runs:

Filter unusable rows first. Drop rows with an empty email or an obvious placeholder before pushing. A mail platform full of blank guest profiles ruins deliverability stats and costs contact-tier money.

Dedupe on id, never on email. Two family members can share one email address, and one person can appear with two addresses over the years. The id is the identity; email is just an attribute of it.

Store the id as the external key in the mail platform. Every platform has an external-id or custom-field slot. Write the Storekeeper id there and upsert on it, so a re-run updates existing profiles instead of duplicating them, and an email change in Storekeeper propagates instead of forking a second profile.

# per drained row
if row.email is empty        -> skip
if seen[row.id]              -> skip (already handled this run)
upsert profile where external_id == row.id
  set email, name, business_name