Skip to content

Iterable

Connect Iterable to JustAI to run AI-optimized copy experiments in your existing campaigns and journeys. JustAI serves the best content variant to each recipient when the message renders and learns from opens, clicks, and conversions.

  1. You create an Iterable Data Feed that calls JustAI. When Iterable renders a message, the Data Feed fetches the best copy variant for that recipient.
  2. Your Iterable template references the values returned by the Data Feed, so each recipient sees the variant JustAI picked for them.
  3. Engagement events (sends, opens, clicks, unsubscribes) flow back to JustAI through an Iterable System Webhook, so JustAI learns which variants win.
  • Your JustAI org slug — usually your company name in lowercase (ask us if you’re not sure).
  • Permission to create Data Feeds and webhooks in your Iterable project.
  • A JustAI API key (create one in Settings → API Keys in JustAI).

You only need to do this once per Iterable project.

  1. In Iterable, create a Data Feed named JustAI.
  2. Set the Data Feed URL (POST) to:
    https://worker.justwords.ai/api/generate/<org_slug>?template_id={{ defaultIfEmpty clientTemplateId templateId }}&tracking_id={{ messageId }}&copy_id={{ copy_id }}&user_id={{ defaultIfEmpty userId email }}
  3. Add a custom header:
    • Name: X-Api-Key
    • Value: your JustAI API key (from Settings → API Keys in JustAI)
  4. Copy the Data Feed ID from Iterable and save it in JustAI:
    • Settings → Integrations → Iterable
    • Paste the Iterable server-side API key and Data Feed ID.

JustAI learns from standard engagement events—sends, opens, clicks, and conversions. To send them, set up a System Webhook in Iterable.

  1. Go to Integrations → System Webhooks.
  2. Click Create Webhook.
  3. For the API URL, enter the following JustAI API endpoint URL (be sure to replace the org_slug with your actual org name):
    1. https://worker.justwords.ai/api/webhook/itbl/<org_slug>?campaignId={{campaignId}}
  4. Click Create.
  5. Select which events you want the webhook to process (for example: Email Click, Email Open, Email Unsubscribe, Email Blast Send).
  6. Save and Enable.

For each campaign or journey you want to optimize, wire an Iterable template to a JustAI template.

  1. In JustAI, open the template and go to Integration Settings.
  2. Select Iterable and set the treatment Template ID and Campaign ID. The treatment is the version that receives JustAI-optimized copy.
  3. If you run a control (a baseline version with your existing copy), set the control Template ID and Campaign ID.
  4. Save changes, then use the JustAI template in your Iterable Journey or Campaign.

The default Data Feed URL works for basic setups. To personalize generated copy based on user attributes, you can pass additional data to JustAI using the attrs query parameter.

Append custom attributes to the Data Feed URL using the attrs.<attribute_name> pattern with Iterable Handlebars fields:

&attrs.<attribute_name>={{ <handlebars_field> }}

For example, to pass a user’s persona:

&attrs.persona={{ persona }}

Chain multiple attributes together:

&attrs.persona={{ persona }}&attrs.plan={{ planType }}&attrs.city={{ city }}

Iterable can also pass nested values (for example, fields under workspace) by updating the Data Feed URL to point to that nested path.

&attrs.userRole={{ workspace.userRole }}

This creates (or updates) an attribute named userRole in JustAI using the nested workspace.userRole value from Iterable.

If the source value is an array, join it into a comma-separated string in the URL so it remains valid:

&attrs.favoriteCategories={{ join workspace.favoriteCategories "," }}

In JustAI, configure the matching attribute as a list attribute so the comma-separated value is interpreted correctly.

You can use Iterable’s Handlebars helpers for fallback values or conditional logic:

&attrs.plan={{ defaultIfEmpty planType "free" }}

In an event-triggered journey or campaign, you can reference fields from the triggering event exactly like a profile attribute—use the same {{ fieldName }} Handlebars syntax, no special prefix required. Before Iterable calls the Data Feed, it replaces each URL parameter with the matching value from the user’s profile or from the event that triggered the campaign.

&attrs.product_category={{ productCategory }}&attrs.item_count={{ itemCount }}

Here’s a complete Data Feed URL with personalization attributes:

https://worker.justwords.ai/api/generate/<org_slug>?template_id={{ defaultIfEmpty clientTemplateId templateId }}&tracking_id={{ messageId }}&copy_id={{ copy_id }}&user_id={{ userId }}&attrs.persona={{ persona }}&attrs.plan={{ defaultIfEmpty planType "free" }}&attrs.loyalty_tier={{ loyaltyTier }}

If you already track custom events in Iterable (for example purchases or signups), you can forward them to JustAI so they count toward your experiment results. Set up a Journey Webhook:

  1. Destination: Custom
  2. Method: POST
  3. Endpoint: https://worker.justwords.ai/api/webhook/events/<org_slug> (be sure to replace the org_slug with your actual org name)
  4. Headers: X-Api-Key
    • You can generate the value for the header by generating an API key here.
  5. Body:
{
"email": "{{email}}",
"workflowId": "{{workflowId}}"
}
  1. Update webhook.
Custom Webhook

To send a custom event, create a new Journey that is triggered by the custom event you want to forward, and then set up a Journey Webhook that points to the webhook defined above.

Event ChainWebhook Details

You can define a Journey per custom event, and we recommend organizing them into a JustAI Export directory.

If your engagement and conversion data lives in a warehouse, your data team can export it to JustAI instead of (or in addition to) webhooks. Share this section with your data team.

JustAI can receive data from Redshift through a shared S3 bucket. This export can contain all relevant custom metrics that you may want to track.

  1. Create an IAM role for the export:
    1. Recommended Permissions: s3:PutObject, s3:ListBucket, s3:GetBucketLocation
    2. Share the ARN role with JustAI. JustAI team will grant read/write permissions to a shared S3 bucket.
  2. Unload data to the shared S3 bucket using Redshift (documentation).
    1. Sample query to export a table
UNLOAD ('
SELECT
event_timestamp,
event_date,
event_name,
user_id_or_email -- Need some way to join against ESP data
FROM public.custom_metrics
')
TO 's3://justwords-metrics-ingest/redshift/{org_slug}'
IAM_ROLE 'arn:aws:iam::<acct>:role/MyRedshiftRole'
PARQUET
PARTITION BY (event_date)
INCLUDE -- keep partition columns in the files
ALLOWOVERWRITE -- overwrite existing keys under the prefix
-- REGION 'us-east-1' -- required if Redshift is in another region
;

The Iterable System Webhook contains the email engagement data like sends, opens, clicks, etc, but it also contains PII data like email addresses, so we can specify exactly which data to send with Redshift as long as the Iterable data is replicated in your data warehouse. Iterable docs on System Webhooks.

For custom events, the main criteria is that the event name is a string (e.g. “purchases”, “signup”), and there must be a key like a user ID or an email that we can use to join the custom event data with the data that we are getting from the ESP (e.g. Iterable, Braze, Customer.io). Usually, what we do is to “weakly attribute” a user’s custom event if it happened within 24-72 hours of an email send. Therefore, the custom metrics & the ESP data need to have a user ID and a timestamp to perform this join.

We usually join the ESP data and the custom events on our side, but since we don’t want to share user IDs either, we can do the join in Redshift so that the exported data is scrubbed.

We can consult on the actual query, but it might look something like this (depending on how the data is stored):

-- Produces a single unified event stream:
-- event_name, event_timestamp, message_id, template_id, campaign_id
-- ESP events pass through; custom events are attributed to the latest prior send within 24h.
WITH iterable_events AS (
SELECT
event_name, -- "emailSend", "emailClick", "emailOpen", etc.
event_timestamp,
message_id, -- Uniquely identifies the message that was sent
template_id, -- Identifier for the Iterable template
campaign_id, -- Identifier for the Iterable campaign
user_id -- NOT shared in the final query
FROM public.iterable
WHERE event_name IN (
-- Can be extended with other metric types
'emailSend','emailOpen','emailClick','emailUnsubscribe'
)
),
iterable_sends AS (
SELECT
user_id,
message_id,
template_id,
campaign_id,
event_timestamp AS send_ts
FROM public.iterable
-- Can be extended with smsSend, pushSend, etc.
WHERE event_name IN ('emailSend')
),
custom_events AS (
SELECT
user_id, -- NOT shared in final query
event_name, -- e.g., 'purchase', 'page_view', 'signup'
event_timestamp AS event_ts
FROM public.custom_events
WHERE event_name IN ('purchase','page_view') -- extend as needed
),
-- Candidate sends within lookback window prior to each custom event
send_candidates AS (
SELECT
ce.user_id,
ce.event_name,
ce.event_ts,
s.message_id,
s.template_id,
s.campaign_id,
s.send_ts,
ROW_NUMBER() OVER (
PARTITION BY ce.user_id, ce.event_name, ce.event_ts
ORDER BY s.send_ts DESC
) AS rn
FROM custom_events ce
JOIN esp_sends s
ON s.user_id = ce.user_id
AND s.send_ts <= ce.event_ts
-- lookback window (can be adjusted)
AND s.send_ts > DATEADD(hour, -24, ce.event_ts)
),
-- Attributes the custom event to the most recent send within 24h
best_send AS (
SELECT *
FROM send_candidates
WHERE rn = 1
)
-- FINAL unified stream
SELECT
e.event_name,
e.event_timestamp,
e.message_id,
e.template_id,
e.campaign_id
FROM esp_events e
UNION ALL
-- Custom events attributed to a send (only those that matched)
SELECT
ce.event_name,
ce.event_ts AS event_timestamp,
b.message_id,
b.template_id,
b.campaign_id
FROM custom_events ce
JOIN best_send b
ON b.user_id = ce.user_id
AND b.event_name = ce.event_name
AND b.event_ts = ce.event_ts
ORDER BY event_timestamp;

JustAI can receive engagement and custom event data exported from your Databricks workspace. We support two secure patterns:

  1. Direct S3 export (recommended): Write a scrubbed, columnar dataset (Parquet) to the shared bucket prefix we provision for your org, e.g. s3://justwords-metrics-ingest/databricks/{org_slug}/....
  2. Delta Sharing (alternative): Share a PII‑scrubbed view to JustAI via Unity Catalog Delta Sharing. We’ll consume the shared table (Databricks‑to‑Databricks or open sharing) and mirror it into our pipeline.

Both approaches avoid sending PII to JustAI. You can perform joins between Iterable system events and your custom events inside your workspace and export only the fields JustAI needs for analytics.

Section titled “Option A (Recommended): Direct S3 Export from Databricks”

1) Access setup

Goal: Allow your Databricks clusters to write to s3://justwords-metrics-ingest/databricks/{org_slug}.

  • Create an AWS IAM role that Databricks can assume for export with these minimum permissions on the bucket/prefix:
    • s3:PutObject, s3:ListBucket, s3:GetBucketLocation
  • Share the role ARN with the JustAI team. We’ll add the bucket policy to permit that role to write under your org’s prefix.
  • (Unity Catalog recommended): Create a Storage Credential for that IAM role and an External Location pointing at your org’s S3 prefix. Grant your writers WRITE FILES on the external location. If you prefer not to use External Locations, you can still write directly to the S3 path with Spark, as long as your cluster has the IAM role attached (instance profile / assume‑role).

2) Unified, scrubbed export model

We receive a single unified event stream with these columns:

  • event_name (string) — e.g., emailSend, emailOpen, emailClick, emailUnsubscribe, purchase, signup, etc.
  • event_timestamp (timestamp) — event time in UTC.
  • event_date (date)DATE(event_timestamp); used for partitioning.
  • message_id (string, nullable) — unique ID for the email/SMS/push message.
  • template_id (string, nullable) — ESP template identifier.
  • campaign_id (string, nullable) — ESP campaign identifier.

3) Sample Spark SQL to build the unified stream

Adjust table names/columns to your warehouse. Assumes you have two tables:

  • Iterable events table: analytics.iterable_events
  • Custom events table: analytics.custom_events
-- Produces a single unified event stream with PII removed.
-- Custom events are weakly attributed to the latest prior send within 24h.
WITH iterable_events AS (
SELECT
event_name, -- "emailSend", "emailOpen", "emailClick", "emailUnsubscribe", ...
event_timestamp,
message_id,
template_id,
campaign_id,
user_id -- present in source, dropped later
FROM analytics.iterable_events
WHERE event_name IN (
'emailSend', 'emailOpen', 'emailClick', 'emailUnsubscribe'
)
),
iterable_sends AS (
SELECT
user_id,
message_id,
template_id,
campaign_id,
event_timestamp AS send_ts
FROM analytics.iterable_events
WHERE event_name = 'emailSend'
),
custom_events AS (
SELECT
user_id, -- present in source, dropped later
event_name, -- e.g., 'purchase', 'signup', 'page_view'
event_timestamp AS event_ts
FROM analytics.custom_events
WHERE event_name IN ('purchase','signup','page_view')
),
-- Candidate sends within 24h before each custom event
send_candidates AS (
SELECT
ce.user_id,
ce.event_name,
ce.event_ts,
s.message_id,
s.template_id,
s.campaign_id,
s.send_ts,
ROW_NUMBER() OVER (
PARTITION BY ce.user_id, ce.event_name, ce.event_ts
ORDER BY s.send_ts DESC
) AS rn
FROM custom_events ce
JOIN iterable_sends s
ON s.user_id = ce.user_id
AND s.send_ts <= ce.event_ts
AND s.send_ts > ce.event_ts - INTERVAL 24 HOURS
),
best_send AS (
SELECT * FROM send_candidates WHERE rn = 1
),
-- ESP passthrough (no PII)
esp_stream AS (
SELECT
event_name,
event_timestamp,
CAST(event_timestamp AS DATE) AS event_date,
message_id,
template_id,
campaign_id
FROM iterable_events
),
-- Custom events attributed to a send (no PII)
attributed_custom AS (
SELECT
ce.event_name,
ce.event_ts AS event_timestamp,
CAST(ce.event_ts AS DATE) AS event_date,
b.message_id,
b.template_id,
b.campaign_id
FROM custom_events ce
JOIN best_send b
ON b.user_id = ce.user_id
AND b.event_name = ce.event_name
AND b.event_ts = ce.event_ts
)
SELECT * FROM esp_stream
UNION ALL
SELECT * FROM attributed_custom;

If you want to extend beyond email (e.g., smsSend, pushOpen), add those to iterable_events and iterable_sends and carry the same logic forward.

4) Scheduling & SLAs

  • Run the export daily. Re‑exporting the most recent 2–3 days is fine.

If you prefer not to write to S3, you can share a read‑only view with JustAI.

1) Build a scrubbed shareable view

Use the same SQL from Option A, but create a view in a governed schema (no PII):

CREATE OR REPLACE VIEW analytics.justwords_unified AS
WITH ... -- (use the SQL from Section 3)
SELECT * FROM esp_stream
UNION ALL
SELECT * FROM attributed_custom;

2) Create a share and add the view

CREATE SHARE IF NOT EXISTS just_words_ingest;
ALTER SHARE just_words_ingest ADD TABLE analytics.justwords_unified;

3) Create a recipient for JustAI and grant access

-- Create a recipient (Databricks-to-Databricks or open sharing)
CREATE RECIPIENT IF NOT EXISTS just_words_recipient;
-- Grant read access to the share
GRANT SELECT ON SHARE just_words_ingest TO RECIPIENT just_words_recipient;
-- Retrieve the activation link or credentials for the recipient
DESCRIBE RECIPIENT just_words_recipient;

Share the activation link (or credential/oidc info) with JustAI. We’ll connect as a recipient and mirror the shared table into our pipeline.

If you later evolve the schema, add a new versioned view (e.g., justai_unified_v2) and add it to the same share to keep compatibility.

4) Refresh cadence

  • Recompute the view continuously (e.g., with a Scheduled Workflow) or materialize to a Delta table that the view selects from.
  • We’ll read fresh snapshots on our schedule.
  • Iterable System Webhooks deliver sends/opens/clicks in near‑real‑time. If you already capture those webhooks into your lakehouse (recommended), use that table as the source for iterable_events.
  • Extend the SQL to include SMS/Push events as needed. The only requirement is that we receive the ESP linkage keys (message_id, template_id, campaign_id) where available.
  • Join and attribution happen inside your workspace. Only the scrubbed columns above are exported/shared.
  • Keep your org’s S3 prefix isolated; we grant write only to that prefix.
  • Small files: Repartition before writing; target 128–512MB per file.
  • Partition skew: If most events occur on a few dates, consider secondary partitioning (e.g., by event_name) or bucketing.
  • Overwrite behavior: Enable dynamic partition overwrite if you re‑run daily partitions.
  • Schema drift: Add new columns as nullable; we handle additive evolution.
  • Option A: IAM role ARN (write), org slug, and (if using UC) the External Location name.
  • Option B: Share name, recipient activation link (or credential), and the fully‑qualified view name.

We’ll confirm ingestion and backfill windows once your first drop/share is available.

JustAI can receive data from Chameleon through webhooks.

Make an API call to JustAI’s Generate API and set the copy onto the Iterable user profile.

Chameleon API Call

Call the endpoint https://api.chameleon.io/v3/observe/hooks/profiles with a body like:

{
"uid": "{{userId}}",
"copy_id_<tour_id>": "{{copy.id}}",
"h1_<tour_id>": "{{copy.vars.h1}}",
"body_<tour_id>": "{{copy.vars.body}}"
}

This stores the copy variables from JustAI on the Chameleon user profile.

In Chameleon’s Integrations page, specify email and copy_id_<tour_id> as additional properties to include on webhook events.

Chameleon Integrations Page

See Can I send additional properties to my connected integrations? in the Chameleon Help Center.

  • Copy isn’t matching the right users — the Data Feed URL’s user_id parameter must use your canonical Iterable identifier. The example URL uses userId, but if your org relies on a different field, update the user_id parameter to match.
  • The Data Feed exists but variants don’t render — confirm you saved the Iterable server-side API key and Data Feed ID in JustAI under Settings → Integrations → Iterable, and that the Data Feed’s X-Api-Key header contains a valid JustAI API key.
  • No engagement data in JustAI — check that the System Webhook is saved and enabled, and that you selected the events it should process (for example: Email Click, Email Open, Email Unsubscribe, Email Blast Send).