Skip to main content
You send iMessage from Customer.io with a journey Webhook action that POSTs to https://app.tuco.ai/api/messages with your Tuco API key, and you get replies back by pointing Tuco’s message webhooks at a webhook-triggered Customer.io journey. Customer.io has no native iMessage channel. Tuco sends from a dedicated line and fires message.sent, message.reply, message.fallback and message.failed, which Customer.io records as events on the person. No Zapier or n8n is needed in between. Overview, architecture and FAQ: tuco.ai/integrations/customer-io.

Journey 1 — send the message

Create an event-triggered journey on your opt-in event (for example form_submitted). Add a Webhook action:
1

Request

Method POST, URL https://app.tuco.ai/api/messages.
2

Headers

Authorization: Bearer tuco_xxxxxxxxxxxxx (API key), Content-Type: application/json, and optionally Idempotency-Key: cio-{{customer.id}}-first so a Customer.io retry never sends twice.
3

Body

Paste the JSON below. Customer.io fills in the Liquid from the profile.
  • recipientEmail links replies to the person. Tuco texts recipientPhone first and stores the email on its lead. Every webhook then carries lead.email, which Journey 2 uses to find the Customer.io profile.
  • contactOwnerEmail assigns the conversation to a rep. Reply notifications go to that person only.
  • Send window: outside 09:00–20:00 in the contact’s timezone, the request still returns 201 with status: "scheduled", and Tuco sends when the window opens.
  • First message = plain text. Links aren’t clickable and attachments are dropped on a first-ever message (limits). Put links and media in a later step.
  • Never send an empty greeting. A body like "Hi , …" or one still containing {{firstName}} is rejected with 400 MALFORMED_NAME_TEMPLATE. The {% if %} above prevents it.
For a follow-up, add a delay and a second Webhook action with the same body plus "respectGap": true. With skipIfReplied: true, Tuco returns 200 with skippedOnly: true and sends nothing if the contact has already replied. See Send Message for every field.

Journey 2 — get replies back into Customer.io

1

Create a webhook-triggered journey

In Customer.io, create a journey with a Webhook trigger and copy its incoming URL.
2

Add a Create Event action

Identify the person by email = {{trigger.lead.email}}. Event name (Liquid): tuco_{{ trigger.event | replace: '.', '_' }}.
3

Map attributes

text, tuco_message_id = {{trigger.messageId}}, tuco_lead_id = {{trigger.leadId}}, phone = {{trigger.lead.phone}}, occurred_at = {{trigger.timestamp}}.
4

Register it in Tuco

Add the URL under Integrations → Webhooks (or Create Webhook) and subscribe to message.sent, message.reply, message.fallback, message.failed and conversation.handover. Use Send test to confirm delivery.
message is a string (the reply text) on message.reply, and the full message object on sent, failed and fallback. Map text with a branch:{% if trigger.event == 'message.reply' %}{{trigger.message}}{% else %}{{trigger.message.message}}{% endif %}
Payload shapes for each event: Message Webhooks.

Test before you start the journeys

  1. Trigger the opt-in event for a test profile with your own mobile number.
  2. Confirm the iMessage arrives, then reply to it.
  3. Check the profile’s activity log for tuco_message_reply.
  4. If nothing arrives, open the webhook action’s delivery in Customer.io. The response body names the reason, for example INVALID_TIMEZONE, a daily cap, or MALFORMED_NAME_TEMPLATE.

Limits to plan around

Each line sends to 50 new contacts and 150 total messages per day, paced at about 7 new contacts per hour (rate limits). Trigger only on real opt-ins. Spam reports, not volume, are what get a line banned (line health). Turn on Customer.io message throttling for the journey, and add lines to scale.