Hightouch
Use the Hightouch HTTP Request destination to send rows from your warehouse to JustAI. You set up two syncs. The profile sync sends one Identify request for each new or changed customer row, which creates or updates that person’s customer profile. The event sync sends one Track request for each new event row, which records a customer event. Hightouch builds each request body from a Liquid template, so you decide exactly which columns JustAI receives.
Owner: Data
Before you begin
Section titled “Before you begin”- Your warehouse is connected to Hightouch as a source, and you can add destinations, models, and syncs.
- You have a JustAI API key for the environment you send to. See Create a key.
- You have your organization slug from your JustAI team. For a non-production environment, your JustAI team also gives you the environment’s path value, which replaces the slug, and its environment key for the
X-JustAI-Environmentheader. To send to more than one environment, see Send to production and a staging environment. - You know which column holds the user ID you agreed to use in JustAI. See Identity and consent.
Prepare your models
Section titled “Prepare your models”Create two models: a profile model with one row for each customer, and an event model with one row for each event. Shape the values in SQL. The Identify and Track APIs reject the wrong types, and Hightouch retries every rejected row on every run until it succeeds.
Profile model
Section titled “Profile model”Set the primary key to user_id.
| Column | Contents |
|---|---|
user_id |
Your user ID for the customer, as text. |
updated_at_iso |
When the row last changed, as ISO 8601 text in UTC ending in Z. Use a value that changes whenever any trait in the row changes. |
email, first_name, plan, lifetime_value |
One column for each trait you send. Use your own trait names. Include email, because JustAI reaches each person in Iterable by this address. |
Event model
Section titled “Event model”Set the primary key to event_id.
| Column | Contents |
|---|---|
event_id |
A unique ID for the event, as text. It becomes the event’s messageId. |
user_id |
The user ID of the customer who did it, as text. Use the same value as in the profile model. |
event_name |
The event name, such as Order Completed. Journeys match it exactly, including case. |
occurred_at_iso |
When the event happened, as ISO 8601 text in UTC ending in Z. |
order_id, order_value, currency |
One column for each event property. Use your own property names. |
Leave out event rows that have no user ID, event name, or time.
Rules for IDs and times
Section titled “Rules for IDs and times”- Cast IDs to text. Hightouch renders a number column as a JSON number, and both APIs reject a number in
userIdormessageId. Cast numeric user IDs and event IDs to text in SQL. - Match user IDs exactly. Remove leading and trailing spaces in SQL. The Identify API trims spaces from the user ID, but the Track API saves it as sent, so a padded ID in the event model doesn’t match the profile.
- Format times in SQL. The APIs accept ISO 8601 text with a time zone, such as
2026-10-01T12:00:00Z. They reject plain SQL timestamps, such as2026-10-01 12:00:00, and epoch numbers.
These expressions turn a UTC timestamp column named occurred_at into ISO 8601 text, such as 2026-10-01T12:00:00.123Z. Check the result in your warehouse before you sync.
FORMAT_TIMESTAMP('%Y-%m-%dT%H:%M:%E3SZ', occurred_at, 'UTC') AS occurred_at_isoTO_VARCHAR(CONVERT_TIMEZONE('UTC', occurred_at), 'YYYY-MM-DD"T"HH24:MI:SS.FF3') || 'Z' AS occurred_at_isoTO_CHAR(occurred_at, 'YYYY-MM-DD') || 'T' || TO_CHAR(occurred_at, 'HH24:MI:SS.MS') || 'Z' AS occurred_at_isoThe Redshift expression assumes occurred_at is a TIMESTAMP that already holds UTC time.
Add the destination
Section titled “Add the destination”-
In Hightouch, open the Destinations overview, select Add destination, choose HTTP Request, and select Continue.
-
Set Base URL to
https://ingest.justwords.ai. -
Under the HTTP headers, enter
X-Api-Keyas the key name, select Secret, and select Add key. Enter your API key as the value.Secret encrypts the value and hides it in Hightouch.
-
For a non-production environment, add a second header,
X-JustAI-Environment, with the environment key from your JustAI team.Leave it out for production. A header with an empty value returns
400. -
Finish creating the destination. JustAI needs no certificates or OAuth.
Create the profile sync
Section titled “Create the profile sync”-
On the Syncs overview page, select Add sync. Choose your profile model and the HTTP Request destination you created.
-
Turn on the Rows added and Rows changed triggers. Leave Rows removed off.
The Identify API has no delete. Configure the two triggers the same way, with the settings in the next three steps.
-
Send one row per request. Don’t choose A batch of multiple rows.
JustAI accepts one JSON object per request and rejects a batch with
400. -
Set the method to
POSTand the path to/api/identify/{org_slug}. Replace{org_slug}with your organization slug in lowercase, or with the environment’s path value for a non-production environment. Don’t end the path with a slash. -
Choose Use JSON editor and paste this template into Define JSON payload. Replace the trait columns with your own.
Identify payload {%- capture message_key -%}{{ row.user_id | json }}|{{ row.updated_at_iso | json }}|{{ row.email | json }}|{{ row.first_name | json }}|{{ row.plan | json }}|{{ row.lifetime_value | json }}{%- endcapture -%}{"userId": {{ row.user_id | json }},"messageId": {{ message_key | sha256 | json }},{%- if row.updated_at_iso %}"timestamp": {{ row.updated_at_iso | json }},{%- endif %}"traits": {"email": {{ row.email | json }},"first_name": {{ row.first_name | json }},"plan": {{ row.plan | json }},"lifetime_value": {{ row.lifetime_value | json }}}} -
Set a conservative starting concurrency and a rate limit in Requests per second or Requests per minute. Confirm both with your JustAI team before the first full run.
Hightouch sends up to 1,000 requests per second or 100 concurrent requests by default. The first run sends every profile, so consider 10 or fewer concurrent requests as a conservative starting point for that run and later profile backfills. This is suggested guidance, not an enforced API limit. Before the first full run, send your JustAI team the number of profiles and your planned concurrency. Your JustAI team confirms the rate or discusses whether a customer-specific file import can meet your needs. See Load your existing customers. With Requests per hour, each run waits a full hour before the next run starts.
-
Set the first run to send existing rows.
The first run then loads every current profile into JustAI. An Identify update that changes a trait can start a journey whose entry is A profile attribute changes, so load your profiles before you publish those journeys.
-
Finish creating the sync. Don’t run it until its test passes. See Test each sync.
How the template works:
messageIdis stable for each version of a row. It is a SHA-256 hash of the user ID, the update time, and every trait in the body. When Hightouch retries a row, it sends the samemessageId, and JustAI applies the update once. When any value changes, the hash changes too, so JustAI treats it as a new update.- Keep
message_keycomplete. When you add a trait to the body, add its column tomessage_key. If two different bodies share onemessageId, the Identify API returns409, and Hightouch retries that row on every run. - The update time must change with the row. If a row changes back to values it had before and
updated_at_isostays the same, the hash matches the earlier update. JustAI treats the row as a repeat and doesn’t apply it. timestamporders updates. JustAI keeps the newest value for each trait, so an update with an oldertimestampdoesn’t overwrite a newer value. The Identify API rejects atimestampmore than five minutes in the future.| jsonwrites each value as valid JSON. It quotes and escapes text, and leaves numbers, booleans, andnullunquoted. Don’t put quotes around it.- An empty column stores
null. Anulltrait replaces the saved value. To keep the saved value when a column is empty, wrap its line in{%- if row.plan != nil %}and{%- endif %}. Hightouch removes the trailing comma that this can leave.
Send your own messageId on Identify when you can give every new update a new ID. Leave messageId out only when your sync tool can’t guarantee a new ID whenever a synced value changes. JustAI then generates a new ID for each call, so it can’t recognize a retry and treats every retry as a new update. When you leave messageId out, always send timestamp, so a delayed retry can’t overwrite a newer value.
With this template, Hightouch can’t guarantee a new ID when updated_at_iso doesn’t change every time a row changes. In that case, delete the capture block and the "messageId" line, and keep the timestamp line.
See Send customer profiles for how each update merges into the profile.
Create the event sync
Section titled “Create the event sync”-
On the Syncs overview page, select Add sync. Choose your event model and the same HTTP Request destination.
-
Turn on Rows added only. Leave Rows changed and Rows removed off.
The Track API records what happened and doesn’t update an accepted event. With Rows changed on, Hightouch would send a corrected row again with the same
messageId, and JustAI would still count one event. -
Send one row per request. Don’t choose A batch of multiple rows.
-
Set the method to
POSTand the path to/api/track/{org_slug}. Put the same value in place of{org_slug}as in the profile sync. -
Choose Use JSON editor and paste this template into Define JSON payload. Replace the property columns with your own.
Track payload {"type": "track","messageId": {{ row.event_id | json }},"userId": {{ row.user_id | json }},"event": {{ row.event_name | json }},"timestamp": {{ row.occurred_at_iso | json }},"properties": {"order_id": {{ row.order_id | json }},"value": {{ row.order_value | json }},"currency": {{ row.currency | json }}}} -
Set the rate limit in Requests per second or Requests per minute.
Each sync has its own limits, so the event sync doesn’t share the profile sync’s limit. With Requests per hour, each run waits a full hour before the next run starts.
-
Set the first run to skip existing rows.
JustAI handles every event sent to the Track API as a new event, whatever its
timestamp. A replayed past event can start or advance any published journey that uses its name. Contact your JustAI team before planning an import of past events. -
Finish creating the sync. Don’t run it until its test passes. See Test each sync.
If your model keeps all properties in one column, replace the properties object with one of these lines:
- For a JSON object column:
"properties": {{ row.properties | json }} - For a column of JSON text:
"properties": {{ row.properties | parse | json }}
To correct or remove events that JustAI already accepted, contact your JustAI team. See Send customer events for how journeys use events.
Test each sync
Section titled “Test each sync”Test each sync’s settings on a separate test model and test sync before the real syncs run.
-
Create a test model for each of your two models. Use the same query, filtered to your test user, such as
WHERE user_id = 'user-1234'. -
Create a test sync for each test model on the same destination, with the same settings as its real sync. Set the first run of both test syncs to send existing rows.
The first run of each test sync then sends your test user’s rows.
-
In each test sync, open the Preview tab. If no sample row has loaded, select Load preview.
The preview shows the rendered body and any template errors.
-
Check the rendered body.
It is one JSON object. IDs are quoted strings, times end in
Z, and numbers aren’t quoted. -
Run each test sync.
-
Open the test sync’s Runs tab and select View run for that run.
Rows JustAI accepted are on the Successful tab. Rows JustAI rejected are on the Rejected tab. Select a row to see its request and JustAI’s response. If a row is rejected, make the same fix to the test and real models or syncs, and run the test sync again. See Watch for rejected rows.
-
Confirm JustAI’s response for a successful row.
The Identify API returns
200:Response {"success": true,"messageId": "50370b24c6946637f55742b9e7d4107fe9211ee59632a7c7afa9c02d7a0b3424","duplicate": false}The Track API returns
202:Response {"message_id": "order-5678","status": "accepted"} -
Delete the test syncs and the test models.
-
Run the profile sync, then the event sync.
Your customers then have profiles before their events arrive.
Watch for rejected rows
Section titled “Watch for rejected rows”Hightouch treats every 4xx and 5xx response as an error and retries the row until it succeeds, either right away or on later runs. A row that JustAI rejects as invalid fails the same way each time. Fix rejected rows in the model instead of letting them retry. Set up Hightouch alerts for both syncs so that you see rejected rows when they start.
| Response | Cause | Fix |
|---|---|---|
400, “Expected string, received number” |
The user ID, event ID, or time rendered as a number. | Cast the column to text in the model. |
400, “Invalid datetime” |
A time has no time zone, uses a space instead of T, or isn’t ISO 8601. |
Format the time in SQL as shown in Rules for IDs and times. |
400, “timestamp cannot be more than 5 minutes in the future” |
The profile’s updated_at_iso is later than the current time, for example because a local time was labeled as UTC. |
Convert the column to UTC before you format it. |
400, “At least one trait is required unless consent is provided” |
The Identify body has an empty traits object. |
Make sure each profile row renders at least one trait. |
400, “Expected object, received array” |
The sync sends A batch of multiple rows. | Send one row per request. |
409, “messageId … was already used for a different call” |
Two different Identify bodies share one messageId. |
Add every trait column to message_key, or leave messageId out. |
For authentication and URL errors, such as 401, 403, 404, Invalid org slug, and Invalid environment, see Errors and retries.
Send to production and a staging environment
Section titled “Send to production and a staging environment”A Hightouch destination holds one API key and one environment header, so each JustAI environment needs its own destination and its own syncs.
- Create one HTTP Request destination for each JustAI environment, with an API key created in that environment. On a non-production destination, also add the
X-JustAI-Environmentheader with the environment key. - Create each environment’s profile sync and event sync on that environment’s destination. In the sync paths, put the environment’s path value in place of
{org_slug}, for example/api/identify/acme_staging. - Name each destination and sync after its environment, such as
JustAI staging. A key, path value, and header that point to different environments return403.
Send profile files to Amazon S3 instead
Section titled “Send profile files to Amazon S3 instead”If you need to export profile files from Hightouch to Amazon S3 instead of sending API requests, discuss a customer-specific import with your JustAI team first. Confirm availability, the format, access requirements, and acceptance checks before configuring an export. See Plan an Amazon S3 file import.
Send events that need to start or advance journeys through the event sync on this page. Contact your JustAI team before planning an import of past events.
Check your work
Section titled “Check your work”-
In Hightouch, open each sync’s Runs tab and select View run for the latest run.
Rows are on the Successful tab, and the Rejected tab is empty. If any row is rejected, fix it in the model as described in Watch for rejected rows, and check that Hightouch alerts are on for both syncs.
-
For the first run of the profile sync, compare the number of rows on the Successful tab with the number of rows your profile model returns.
The two numbers match, so every current profile reached JustAI.
-
Read a synced profile back with the Profiles API. The command uses the
JUSTAI_ORG_SLUG,JUSTAI_ENVIRONMENT, andJUSTAI_API_KEYshell variables from Test your connection. For a non-production environment,JUSTAI_ORG_SLUGholds the environment’s path value andJUSTAI_ENVIRONMENTholds its environment key. For production,JUSTAI_ENVIRONMENTis empty, andcurlleaves the header out.Terminal window curl "https://ingest.justwords.ai/api/profiles/$JUSTAI_ORG_SLUG/user-1234" \-H "X-Api-Key: $JUSTAI_API_KEY" \-H "X-JustAI-Environment: $JUSTAI_ENVIRONMENT"The response is
200, andfieldsholds the traits from the row. If a change isn’t there yet, wait a minute and read again. You can also search for the user ID on Users in JustAI and open the row. -
In JustAI, open Events > Custom events and find your event names.
New events can take time to appear. If an accepted event still doesn’t appear after you refresh, contact your JustAI team.
