> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuco.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# How to Send iMessage from Customer.io

> Send 1:1 iMessage from a Customer.io journey with a Webhook action, and record Tuco replies, sends and failures back on the person as Customer.io events.

**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](https://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:

<Steps>
  <Step title="Request">
    Method `POST`, URL `https://app.tuco.ai/api/messages`.
  </Step>

  <Step title="Headers">
    `Authorization: Bearer tuco_xxxxxxxxxxxxx` ([API key](/api-reference/api-keys)), `Content-Type: application/json`, and optionally `Idempotency-Key: cio-{{customer.id}}-first` so a Customer.io retry never sends twice.
  </Step>

  <Step title="Body">
    Paste the JSON below. Customer.io fills in the Liquid from the profile.
  </Step>
</Steps>

```json theme={null}
{
  "message": "{% if customer.first_name != blank %}Hi {{customer.first_name}}{% else %}Hi there{% endif %}, thanks for reaching out. Any questions I can answer for you here?",
  "recipientPhone": "{{customer.phone}}",
  "recipientEmail": "{{customer.email}}",
  "recipientName": "{{customer.first_name}} {{customer.last_name}}",
  "contactOwnerEmail": "{{customer.owner_email}}",
  "sendWindowStart": "09:00",
  "sendWindowEnd": "20:00",
  "timezone": "{{customer.timezone | default: 'America/New_York'}}",
  "skipIfReplied": true
}
```

* **`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](/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](/api-reference/endpoint/send-message) for every field.

## Journey 2 — get replies back into Customer.io

<Steps>
  <Step title="Create a webhook-triggered journey">
    In Customer.io, create a journey with a **Webhook** trigger and copy its incoming URL.
  </Step>

  <Step title="Add a Create Event action">
    Identify the person by **email** = `{{trigger.lead.email}}`. Event name (Liquid): `tuco_{{ trigger.event | replace: '.', '_' }}`.
  </Step>

  <Step title="Map attributes">
    `text`, `tuco_message_id` = `{{trigger.messageId}}`, `tuco_lead_id` = `{{trigger.leadId}}`, `phone` = `{{trigger.lead.phone}}`, `occurred_at` = `{{trigger.timestamp}}`.
  </Step>

  <Step title="Register it in Tuco">
    Add the URL under **Integrations → Webhooks** (or [Create Webhook](/api-reference/endpoint/create-webhook)) and subscribe to `message.sent`, `message.reply`, `message.fallback`, `message.failed` and `conversation.handover`. Use **Send test** to confirm delivery.
  </Step>
</Steps>

<Warning>
  `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 %}`
</Warning>

| Tuco webhook | Customer.io event | Typical use |
| - | - | - |
| `message.sent` | `tuco_message_sent` | Mark the step done |
| `message.reply` | `tuco_message_reply` | Exit nurture journeys, alert the owner, branch on the text |
| `message.fallback` | `tuco_message_fallback` | No iMessage on the number: send SMS or email from Customer.io |
| `message.failed` | `tuco_message_failed` | Alert ops, retry on another channel |
| `conversation.handover` | `tuco_conversation_handover` | AI responder handed off to a person |

Payload shapes for each event: [Message Webhooks](/api-reference/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](/features/rate-limits)). Trigger only on real opt-ins. Spam reports, not volume, are what get a line banned ([line health](/troubleshooting/line-health-and-restrictions)). Turn on Customer.io message throttling for the journey, and add lines to scale.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.