Skip to content

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

  • 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-Environment header. 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.

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.

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.

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.

  • Cast IDs to text. Hightouch renders a number column as a JSON number, and both APIs reject a number in userId or messageId. 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 as 2026-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_iso

The Redshift expression assumes occurred_at is a TIMESTAMP that already holds UTC time.

  1. In Hightouch, open the Destinations overview, select Add destination, choose HTTP Request, and select Continue.

  2. Set Base URL to https://ingest.justwords.ai.

  3. Under the HTTP headers, enter X-Api-Key as 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.

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

  5. Finish creating the destination. JustAI needs no certificates or OAuth.

  1. On the Syncs overview page, select Add sync. Choose your profile model and the HTTP Request destination you created.

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

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

  4. Set the method to POST and 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.

  5. 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 }}
    }
    }
  6. 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.

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

  8. Finish creating the sync. Don’t run it until its test passes. See Test each sync.

How the template works:

  • messageId is 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 same messageId, and JustAI applies the update once. When any value changes, the hash changes too, so JustAI treats it as a new update.
  • Keep message_key complete. When you add a trait to the body, add its column to message_key. If two different bodies share one messageId, the Identify API returns 409, 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_iso stays the same, the hash matches the earlier update. JustAI treats the row as a repeat and doesn’t apply it.
  • timestamp orders updates. JustAI keeps the newest value for each trait, so an update with an older timestamp doesn’t overwrite a newer value. The Identify API rejects a timestamp more than five minutes in the future.
  • | json writes each value as valid JSON. It quotes and escapes text, and leaves numbers, booleans, and null unquoted. Don’t put quotes around it.
  • An empty column stores null. A null trait 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.

  1. On the Syncs overview page, select Add sync. Choose your event model and the same HTTP Request destination.

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

  3. Send one row per request. Don’t choose A batch of multiple rows.

  4. Set the method to POST and the path to /api/track/{org_slug}. Put the same value in place of {org_slug} as in the profile sync.

  5. 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 }}
    }
    }
  6. 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.

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

  8. 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’s settings on a separate test model and test sync before the real syncs run.

  1. 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'.

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

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

  4. Check the rendered body.

    It is one JSON object. IDs are quoted strings, times end in Z, and numbers aren’t quoted.

  5. Run each test sync.

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

  7. 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"
    }
  8. Delete the test syncs and the test models.

  9. Run the profile sync, then the event sync.

    Your customers then have profiles before their events arrive.

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-Environment header 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 return 403.

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.

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

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

  3. Read a synced profile back with the Profiles API. The command uses the JUSTAI_ORG_SLUG, JUSTAI_ENVIRONMENT, and JUSTAI_API_KEY shell variables from Test your connection. For a non-production environment, JUSTAI_ORG_SLUG holds the environment’s path value and JUSTAI_ENVIRONMENT holds its environment key. For production, JUSTAI_ENVIRONMENT is empty, and curl leaves 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, and fields holds 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.

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