Skip to content

Customer.io

Connect Customer.io to JustAI to run AI-optimized copy experiments in your existing campaigns. JustAI serves the best content variant to each recipient at send time and learns from opens, clicks, and conversions.

  1. A Webhook Action in your Customer.io campaign asks JustAI for content for each recipient.
  2. JustAI’s response is stored as an attribute on the customer profile or the journey run.
  3. Your message renders that content with Liquid, so each recipient sees the variant JustAI picked for them.
  4. Customer.io reporting events (sends, opens, clicks, conversions) flow back to JustAI, so it learns which variants win.
  • Confirm which Customer.io workspace (Production vs other) you’re connecting.
  • Your JustAI org slug — usually your company name in lowercase (ask us if you’re not sure).
  • Make sure you have permissions to create API credentials and webhooks in Customer.io.

This is one-time setup — all templates will share this configuration. Once it’s done, JustAI receives the reporting events it needs to measure performance (send/open/click/conversion) and power downstream workflows.

Create an App API key in your Customer.io workspace:

  • Navigate to Workspace settingsAPI and Webhook CredentialsApp API Keys.
  • Suggested values:
    • Name: JustAI
    • Workspace: Production (or your chosen workspace)

Customer.io API Key Dashboard

Step 2 — Save the Customer.io key in JustAI

Section titled “Step 2 — Save the Customer.io key in JustAI”

In the JustAI console:

  1. Open Org Integration Settings.
  2. Select Customer.io as the ESP integration.
  3. Choose the appropriate workspace (for example, Production).
  4. Paste the Customer.io API key and save changes.

Console Customer.io API Settings

Generate a JustAI API key.

Store the key in a password manager. You’ll use this later for the Customer.io Webhook Action calls in your campaigns.

Create a Reporting Webhook in Customer.io so campaign events flow back to JustAI:

  1. Go to Data & IntegrationsIntegrationsAdd IntegrationReporting Webhook.
  2. Fill out the form:
    • Webhook Name: JustAI
    • Endpoint: https://worker.justwords.ai/api/webhook/cio/<org_slug>
      • Replace <org_slug> with your org’s name.
    • Events: select the events you want JustAI to receive (start with send/open/click/conversion).
    • Options: Send only the first time the event occurs.
  3. Press Save and Enable Webhook.

CIO Webhook Settings

Follow these steps for each campaign you want to optimize with JustAI.

  1. In Customer.io, open the campaign workflow and set up your control/treatment split — control is your existing message, treatment is the JustAI-optimized version:
    • Control: existing message.
    • Treatment: a Webhook Action followed by the JustAI message.
  2. Configure the Webhook Action request:
    • Method: POST
    • URL: https://worker.justwords.ai/api/generate/<org_slug>
    • Headers:
      • X-Api-Key: <JUSTAI_API_KEY>
      • Content-Type: application/json
    • Body: use the payload recommended in JustAI (Template → Integration Settings → Customer.io).
  3. Configure the Response tab:
    • Attribute name: jw_<template_id>
    • Value: {{ response.copy | json }}
    • Source: choose customer attribute or journey trigger data depending on your attribute scope (see Attribute scope below).
  4. In JustAI, open the template Integration Settings and set:
    • Campaign ID
    • Webhook Action ID (and Control Action ID if you use multiple controls)
    • Attribute Scope (if enabled — see below)
  5. Save changes, then reference the stored data in your Customer.io message using the appropriate Liquid prefix:
    • Customer attributes: {{ customer.jw_<template_id>.vars.<var_name> }}
    • Journey attributes: {{ journey.jw_<template_id>.vars.<var_name> }}

When the JustAI webhook returns a response, Customer.io stores it as an attribute that you reference in your message Liquid. There are two places this data can live:

ScopeLiquid prefixStored onBest for
Customer attributescustomer.jw_*The customer profileSimple setups, data needed across multiple journeys
Journey attributesjourney.jw_*The current journey runMost setups — avoids bloating the customer profile with per-template data

Journey attributes are scoped to a single journey execution and are automatically cleaned up. Customer attributes persist on the profile indefinitely. For most use cases, journey attributes are recommended to keep customer profiles lean.

For more details, see the Customer.io docs on journey attributes and the journey attributes release notes.

Per account (default): In JustAI, go to Org Integration Settings and set the Default Attribute Scope on your Customer.io workspace. All new templates will inherit this default.

Per template (override): In the template’s Integration Settings, the Attribute Scope selector lets you override the account default. This is useful when migrating — existing templates can stay on customer while new ones use journey.

The webhook response configuration in Customer.io differs slightly depending on the scope:

  • Customer attributes: In the Response tab, click “Set up an attribute”, choose the attribute name, and enter jw_<template_id>.
  • Journey attributes: In the Response tab, click “Set up an attribute”, choose journey trigger data as the source, and enter jw_<template_id>.

The default webhook body works for basic setups. For personalization, pass additional user attributes to JustAI using the attrs field in the request body. JustAI can auto-sync these attributes to keep your personalization data up to date.

Add an attrs object to your webhook body:

{
"attrs": {
"<attribute_name>": "{{ customer.<field_name> }}"
}
}

For example, to pass a user’s persona:

{
"attrs": {
"persona": "{{ customer.persona }}"
}
}

Pass multiple attributes in a single request:

{
"attrs": {
"persona": "{{ customer.persona }}",
"plan": "{{ customer.plan_type }}",
"city": "{{ customer.city }}"
}
}

Use Liquid conditionals to provide default values when attributes might be missing:

{% if customer.plan_type %}{{ customer.plan_type }}{% else %}free{% endif %}

Anything referenceable in Customer.io Liquid is available. You can reference nested objects, arrays, and relationships:

  • {{ customer.company.name }} - nested object property.
  • {{ customer.tags | first }} - first item in an array.
  • {{ customer.subscription.status }} - related object property.

For event-triggered campaigns, event data is accessible via {{ event.<field> }}:

{
"attrs": {
"product_category": "{{ event.product_category }}",
"order_value": "{{ event.order_value }}"
}
}

A complete webhook body with required fields and custom attributes:

{
"template_id": "<template_id>",
"user_id": "{{ customer.id }}",
"tracking_id": "{{ message.journey_id }}",
"attrs": {
"persona": "{{ customer.persona }}",
"plan": "{% if customer.plan_type %}{{ customer.plan_type }}{% else %}free{% endif %}",
"company_name": "{{ customer.company.name }}"
}
}

JustAI supports two ways to pass user data: attrs and fields.

ParameterPurposeUse When
attrsAttributes used for ranking and filtering variantsYou want JustAI to select different content based on user segments (e.g., persona, plan type).
fieldsPersonalization fields returned as hydrated stringsYou need to insert user-specific values (e.g., first name) into the generated content without affecting variant selection.

Use fields for simple personalization like names or account details:

{
"fields": {
"first_name": "{{ customer.first_name }}",
"account_number": "{{ customer.account_id }}"
}
}

You can combine both in the same request:

{
"attrs": {
"persona": "{{ customer.persona }}",
"plan": "{{ customer.plan_type }}"
},
"fields": {
"first_name": "{{ customer.first_name }}"
}
}

In this example, JustAI uses persona and plan to select the best variant, then hydrates first_name into the returned content.

For more details, see the Customer.io docs on Webhook Actions and Liquid personalization.

Customer.io conversion metrics are tied to a campaign. If you need additional metrics, you can send custom events to JustAI for correlation with message sends within a time window.

Options:

  • Daily batch ingestion via S3 (write data to a shared S3 bucket).
  • Streaming ingestion via a JustAI events webhook API (similar shape to the Customer.io webhook endpoint above).

Please reach out to our team if you need to add custom events and we can help you integrate them.

JustAI can export a record of which content each user received in your JustAI-integrated Customer.io campaigns, so your data team can analyze results in your own warehouse. The easiest strategy is a daily data export to a shared S3 bucket on your AWS account.

Each record reflects one JustAI API call, with a user ID and a tracking ID that uniquely identifies an email/notification in Customer.io.

{
"event_timestamp": <unix_timestamp>,
"user_id": <string>, // Customer.io
"tracking_id": <string>, // Customer.io
"copy_id": <uuid_string>, // JustAI
"template_id": <string>, // JustAI
// Record of strings, but depends on the template
"vars": {
"subject": <string>,
"preheader": <string>,
"body": <string>
},
// Record of strings, but depends on the template
"attrs": {
"persona": <string>,
"age": <string>
}
}

In JustAI, each variant has a UUID (copy_id) and a template ID. A template ID corresponds 1:1 with an email / push / etc within a Customer.io campaign, but there can be many variants per template.

The journey ID & action ID uniquely identifies an instance of an email / push / etc and is generated by Customer.io. In our dashboards, we’ll be aggregating the engagement metrics produced by Customer.io but grouped by copy_id and date to see the performance of each variant over time.

This is just a default, and there may be other preferred approaches (direct to Snowflake, etc).

  1. JustAI to provision an ARN role that will read/write to the shared AWS bucket.
  2. Client to create bucket or path in existing bucket and grant read/write access to the role (1)
  3. JustAI to export a backfill of data & to set up a daily export for new records.
  4. Client to transfer data from S3 into Snowflake (for example).

This is just a default, and there may be other preferred approaches (Avro, etc.).

  • The exported data to be in Parquet and written to a partitioned path like ”…/YYYY/MM/DD/HH”
  • Backfills to be run adhoc & would overwrite any existing data.
  • The copy variables can be modified in the frontend, so the UUID => vars could be different. It’s generally the case that we will not modify them once they are being served unless there is a typo / etc.
  • The copy metadata could be set up as a separate table rather than flattening them if that is easier for downstream analysis / better storage.
  • Retention can be handled as a bucket policy.
  • Webhook never fires — confirm the Reporting Webhook is enabled in Customer.io and the endpoint is correct (including <org_slug>).
  • No events arriving — confirm you selected the relevant events and that you are sending emails/triggering events in the correct workspace.
  • Credentials issues — re-check which Customer.io workspace you created the API key in, and that the same workspace is selected in JustAI.
  • {{ response.copy | json }} renders blank — confirm your workspace is on the upgraded Customer.io Liquid version. Older Liquid versions silently ignore the json filter.
  • Message renders blank values — the attribute scope in JustAI must match how you configured the Response tab in Customer.io. If JustAI generates journey.jw_* Liquid but you stored the response as a customer attribute (or vice versa), the values come back blank.