A growth engineer at a 30-person SaaS company opens a ticket: marketing wants to sync customer data to Braze so the lifecycle flows can target on current plan, last login, and trial expiry. The data lives in Postgres, Stripe, and HubSpot. There is no warehouse. There is no Segment subscription. Someone is about to write a Python script that pulls from three places, resolves user IDs, and POSTs to the Braze users/track endpoint every fifteen minutes.
That script will work. For a while.
The decision to sync customer data to Braze with a custom ingest job is the most common path to messaging data, and it is also the one that most quietly accumulates failure modes. Token rotations, attribute type drift, identity changes after a user signs in with a different email, deletes that never propagate. None of these show up as errors in your monitoring. They show up as a customer who keeps getting onboarding emails six months after they've churned.
This guide walks through what the Braze REST API actually expects on the ingest side, the gotchas around attribute typing and identifier resolution, and how to keep your unified customer profile in sync with Braze without writing the script in the first place.
Why teams write custom jobs to sync customer data to Braze
Braze is a messaging engine. It is excellent at sending email, push, SMS, and in-app messages based on user attributes and event triggers. It is not a customer data store in the same way a CRM or CDP is. The user model in Braze exists to make messaging decisions, not to be your source of truth.
That mismatch is what creates the ingest problem. Your source of truth is your application database, your billing system, and your CRM. Braze needs a denormalized view of each user with attributes that map to the segments and campaigns your lifecycle team wants to run. Getting from one to the other usually means glue code.
The three common approaches all have sharp edges:
Webhook from your app on every meaningful event. Fast and reliable for events you remember to instrument. Useless for attributes that change without an explicit event, like a Stripe webhook you forgot to forward or a HubSpot field a sales rep edited.
Nightly batch script that exports a CSV and uploads to Braze. Easy to write. Now your messaging is 24 hours behind reality, which is fine until you launch a trial-ending campaign that sends to people who already converted.
Reverse ETL from a warehouse. Architecturally clean if you have a warehouse. If you don't, standing one up just so Braze can read fresh user attributes is a six-month detour for a problem that should take an afternoon.
Most teams pick one, accept the tradeoffs, and move on. The cost shows up in the second year when someone has to debug why a customer was billed for the Pro plan in Stripe but is still receiving "upgrade to Pro" emails from Braze.
What the Braze REST API expects from a data sync
Any Braze API sync is almost entirely about one endpoint: /users/track. That's where you send data to Braze on the user attribute side. It accepts up to 75 user objects per request, each with an identifier and a set of custom attributes. It also accepts events and purchases in the same payload, which matters if you want to drive trigger-based campaigns from the same sync.
A typical payload looks like this:
A few things matter here that the Braze API docs cover but most ingest scripts miss:
external_idis the recommended primary key. Email works as an alias but is mutable. If a user updates their email, an email-keyed sync will create a duplicate Braze user. External_id is stable.Attribute types are inferred on first write. If you write
mrr_centsas a number on user A and as a string on user B, Braze stores them as different types. Filters in segments will misbehave. Pick a type per attribute and enforce it at the source.Arrays must be homogeneous and capped at 100 items. Nested objects are accepted but should stay shallow. Braze segmentation flattens nested fields up to one level deep, and anything beyond that is opaque to the campaign builder.
Rate limits are per workspace, not per endpoint. The default is 250,000 requests per hour. A naive script that POSTs one user per request will burn through that on a mid-size sync. Batch 50-75 users per request.
For the official spec, the Braze users/track endpoint reference is the source of truth. Don't trust example payloads from blog posts (including this one) over the docs.
Map unified profiles when you sync customer data to Braze
Most of the work in a Braze ingest is not the HTTP call. It's deciding which fields from which source systems become which Braze user attributes. Get this wrong and the campaigns you build will fire on stale or contradictory data.
A useful mental model is to treat the Braze user record as a denormalized projection of your unified customer profile, not as another copy of your CRM. Some attributes come straight from your billing system because Braze needs them fresh:
Braze attribute | Source system | Update cadence |
|---|---|---|
| Stripe subscriptions | Real-time on subscription.updated |
| Stripe subscriptions | Real-time |
| Application database | Every 5 minutes |
| Stripe or app database | On change |
| HubSpot or CRM | Every 15 minutes |
| Survey tool | On submission |
Two patterns are worth applying regardless of which source you're pulling from.
First, prefix attributes by lifecycle role rather than by source. billing_plan is more useful than stripe_plan because the lifecycle team thinks in billing terms, not in source-system terms. If you later swap Stripe for a different billing provider, the attribute name doesn't lie.
Second, write only the attributes that messaging actually uses. Braze is not a backup of your customer table. Every attribute you write becomes a field someone has to think about in segments and filters. Twenty well-chosen attributes serve lifecycle better than two hundred fields piped across mechanically.
Identifier resolution is where most custom scripts break down. A real user is user_abc123 in your app, cus_KkLm99 in Stripe, contact ID 0011a00001 in HubSpot, and ada@example.com in email. The Braze record needs external_id: user_abc123 and emails and Stripe IDs as auxiliary attributes. A unified profile, by definition, already knows the mapping. A script has to rediscover it on every run, which is where the duplicate users come from.
Where most Braze data sync pipelines silently break
The first version of any Braze ingest works. The second year is where the failure modes accumulate. Four show up in roughly that order:
Identifier drift. A user signs up with a personal email, then later changes to a work email after their company buys a team plan. The custom script writes the new email to Braze without updating external_id, and now Braze has two users with overlapping data. Messaging frequency caps don't apply across the duplicate. The customer gets two of every email for a month.
Type coercion on attributes. Six months in, someone adds a new code path that writes plan_tier as the number 2 instead of the string "team". Braze accepts it. Segments that filter on plan_tier = "team" quietly drop those users from the campaign. Nobody notices until a quarterly review.
Stale users that never get deleted. A customer churns. The script stops writing their attributes, but the user remains in Braze, ages out of lifecycle campaigns into win-back flows, and continues to count against your monthly active user billing. The fix is to call /users/delete on churn, which a surprising number of ingest scripts never implement.
Field-level overwrites. A sales rep manually corrects a misspelled name in Braze. The next sync run overwrites it with the original misspelling from the source. Without field-level change tracking, the script can't tell the difference between a field that hasn't changed and a field someone else updated, so it writes everything every time.
Each of these is fixable in a custom script, but the cost adds up. You're now maintaining an identity resolver, a type validator, a delete propagator, and a field-level diff engine. At that point you've built half a customer data platform, badly, just to keep Braze accurate.
How to sync customer data to Braze without writing code
The premise of a unified customer profile is that the four problems above get solved once, not once per destination. You define what a customer is, you point your sources at it, and any tool you sync to inherits the same identity model, the same type discipline, and the same change tracking.
In Oneprofile, syncing customer data to Braze looks like this:
Connect the sources. Postgres or your application database, Stripe, HubSpot, your support tool. Anything that holds attributes Braze needs. Oneprofile reads schemas at connect time so you don't predeclare what's coming.
Define or accept the unified profile. Identity resolution merges records by email, phone, and external IDs. The profile is the canonical view of each customer.
Connect Braze as a destination. Point Oneprofile at your Braze REST API key. The integration is bidirectional, so attributes flowing back from Braze (campaign clicks, subscription state) can enrich the profile in turn.
Map fields. Pick which profile attributes become which Braze user attributes. The mapper is type-aware, so you can't accidentally write a number into a field Braze already infers as a string.
Let it run. Real-time on supported sources, every 5-15 minutes elsewhere. Deletes propagate. Identity merges propagate. Failed records show up in a queue with the exact error and a retry button, not silently in a log file nobody reads.
The whole setup is an afternoon, not a sprint. Closer to the time it takes to write the first version of a custom script, except this one doesn't accrue maintenance debt as the source systems change shape.
There is one case where a custom script is still the right answer. If your only Braze attribute is a single event triggered by a single user action in a single source system, and you have engineering capacity to maintain it, write the webhook. Don't reach for infrastructure you don't need. But the moment a second source enters the picture, or a second attribute, or a second engineer joins the team, the calculus changes fast.
The lifecycle team's question was never "how do I write a Braze ingest job." It was "why is my Pro-tier upgrade email going to people who already upgraded." That's a data sync problem, not a Braze problem, and it deserves a data sync answer.
What identifier should I use to sync customer data to Braze?
Does Braze support array and nested user attributes?
How do I delete a user from Braze when they leave my product?
Can I send Stripe and HubSpot data to Braze without a warehouse?
How often should a Braze data sync run?
