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

# Send WhatsApp templates and Flows from Customer.io

> Send approved WhatsApp templates and Flows from Customer.io campaigns with the Flowella API, and optionally sync inbound WhatsApp messages back to Customer.io.

Flowella connects to Customer.io through Customer.io's webhook actions and the Flowella REST API. A Customer.io campaign calls Flowella to send an approved WhatsApp template, including templates with a WhatsApp Flow button, to each person who reaches that step. If you also want WhatsApp replies in Customer.io, Flowella can send each inbound message to a Customer.io webhook-triggered automation that you control.

> [!NOTE]
>
> There is no app to install in Customer.io. You build both directions with standard Customer.io actions, so you decide exactly which requests run and which attributes change.

## How the integration works

| Direction | What runs | What it changes |
| - | - | - |
| Customer.io to Flowella | A **Send and receive data** (webhook) action in your campaign calls `POST /v1/templates/send` for the person in the campaign. | Flowella queues one WhatsApp template message. Nothing is written to Customer.io. |
| Flowella to Customer.io (optional) | A Flowella outbound webhook sends `message.received` events to a webhook-triggered automation in Customer.io. | Only the person attributes you map in your own **Create or Update Person** action. |

Flowella never calls the Customer.io API. In the optional inbound direction, Flowella only posts events to the webhook URL you give it, and your automation decides what to do with them.

```mermaid theme={null}
flowchart LR
    A["Customer.io: person reaches the webhook action"] --> B["Flowella: POST /v1/templates/send"]
    B --> C["Meta: WhatsApp message delivered"]
    C --> D["Contact replies on WhatsApp"]
    D --> E["Flowella: message.received webhook"]
    E --> F["Customer.io: webhook-triggered automation"]
    F --> G["Customer.io: Create or Update Person"]
```

## Before you start

* A Flowella organisation with a connected WhatsApp number. See [Setup sequence](https://knowledge.flowella.io/meta/setup-sequence.md).
* An approved WhatsApp template in Flowella. See [Templates](https://knowledge.flowella.io/app/templates.md).
* The **Owner** or **Admin** role in Flowella, to create an API key and, for the inbound sync, a webhook. See [Team and roles](https://knowledge.flowella.io/settings/team.md).
* A Customer.io workspace where you can create campaigns or automations with webhook actions.
* Each person's WhatsApp number stored on their Customer.io profile in international format, such as `+447700900123`. See [Phone number format](https://knowledge.flowella.io/hubspot/phone-number-format.md).
* Each person's agreement to receive WhatsApp messages from you. Meta requires opt-in before you message anyone. See Meta's [opt-in guidance](https://developers.facebook.com/documentation/business-messaging/whatsapp/getting-opt-in).

## Send a WhatsApp template from Customer.io

**1. Create a Flowella API key**

In Flowella, go to **Settings**, then **API keys**, and create a live key with a name such as `Customer.io production`. Copy it straight away: Flowella shows it only once. See [API keys](https://knowledge.flowella.io/settings/api-keys.md).

**2. Find the template and channel IDs**

Call `GET /v1/templates?status=APPROVED` with your key. Note the template's `id` and `wabaId`, and the `whatsappChannelId` of the number you want to send from. See [Multi-channel](https://knowledge.flowella.io/essentials/multi-channel.md) if you have several numbers.

```bash theme={null}
curl "https://api.flowella.io/v1/templates?status=APPROVED" \
  -H "Authorization: Bearer $FLOWELLA_API_KEY"
```

**3. Add a webhook action to your campaign**

In your Customer.io campaign workflow, add a **Send and receive data** action at the point where the WhatsApp message should go out. See Customer.io's guide to [webhook actions](https://docs.customer.io/webhooks-action).

**4. Set the request**

Set the method to `POST` and the URL to `https://api.flowella.io/v1/templates/send`. Add these headers:

| Header | Value |
| - | - |
| `Authorization` | `Bearer ` followed by your Flowella API key |
| `Content-Type` | `application/json` |

**5. Write the request body**

Paste this body and replace the IDs with your own. Change `customer.phone` to the attribute that holds the person's WhatsApp number.

```json theme={null}
{
  "templateId": "1234567890123456",
  "templateWabaId": "102290129340398",
  "whatsappChannelId": "clxxxxxxxxxxxxxxxxxxxxxxxx",
  "recipients": [
    {
      "phone": "{{customer.phone}}",
      "bodyParameters": ["{{customer.first_name}}"]
    }
  ]
}
```

`bodyParameters` fills the template's body variables in order: the first value goes into `{{1}}`, the second into `{{2}}`, and so on. Leave it as an empty list, `[]`, if the template has no body variables.

**6. Test the action**

Pick a test person with your own WhatsApp number and select **Test response** in Customer.io. Add an `Idempotency-Key` header with a value you haven't used before, such as `test-2026-11-11-01`, and change it for every test. See [Repeat sends and idempotency](#repeat-sends-and-idempotency).

A `202` response with `"status": "accepted"` means Flowella queued the send. It doesn't confirm delivery: check the template's **Statistics** tab in Flowella for the result.

**7. Remove the test header and start the campaign**

Delete the `Idempotency-Key` header you used for testing, or replace it with a value that is different for every person. Then start the campaign.

### Template options you can set per person

The request body accepts these fields for each recipient. The [API introduction](https://knowledge.flowella.io/api-reference/introduction.md) and the [OpenAPI document](https://api.flowella.io/v1/openapi.json) describe the full request.

| Field | Use it for |
| - | - |
| `phone` | The person's WhatsApp number. Required. |
| `bodyParameters` | Values for the body variables, in order. Up to 20. |
| `buttonParameters` | Values for URL button variables (`subType` `url`), Flow buttons (`subType` `flow`) or copy-code buttons (`subType` `copy_code`). |

For a template with an image, video or document header, you can set a public file link in `headerMedia.url` at the top level of the body.

### Repeat sends and idempotency

`POST /v1/templates/send` protects you from sending the same message twice, which matters because Customer.io retries a webhook action when it doesn't get a `2xx` response:

* Without an `Idempotency-Key` header, Flowella uses a hash of the request body as the key. A byte-identical body within 7 days returns the original `id` and sends nothing. Because each person's body contains their own number, a normal campaign sends to everyone once.
* With an `Idempotency-Key` header, any request with the same key within 7 days returns the original `id` and sends nothing, even if the body changed.
* Separately, Flowella skips a second send of the same template to the same number within 15 seconds.

> [!WARNING]
>
> Never leave a fixed `Idempotency-Key` value on a live campaign action. Only the first person gets the message: every later request reuses the key, so Flowella returns the first `id` and sends nothing. Remove the header before you start the campaign, or build a value that differs for each person, such as a campaign name followed by `{{customer.id}}`.

The same rules explain most failed tests. Sending the same template to the same test person again produces the same body, so Flowella sends nothing unless you change the `Idempotency-Key` value each time.

### Sending to a large segment

Each request from Customer.io queues one send, and Flowella delivers it in the background, retrying temporary WhatsApp errors. Two limits apply to large campaigns:

* **Meta's messaging limit.** Meta caps how many people you can message outside the 24-hour window in a moving 24 hours. See [Messaging limits](https://knowledge.flowella.io/meta/messaging-limits.md).
* **Your API key's rate limit.** Requests over the limit return `429` with a `Retry-After` header. See [Rate limits](https://knowledge.flowella.io/api-reference/introduction.md#rate-limits).

Spread large sends out with Customer.io's own sending controls, and follow progress on the template's **Statistics** tab. Flowella skips anyone who opted out on the number you send from. See [Opt-outs](https://knowledge.flowella.io/app/opt-outs.md).

## Sync inbound WhatsApp messages to Customer.io (optional)

This part sends each WhatsApp message a contact sends you to Customer.io, so you can update the person or start other campaigns from it.

**1. Create a webhook-triggered automation in Customer.io**

Create a webhook-triggered campaign or automation and copy the webhook URL Customer.io generates. See Customer.io's guide to [webhook campaigns](https://docs.customer.io/journeys/webhook-triggered-campaigns/).

**2. Add an outbound webhook in Flowella**

In Flowella, go to **Settings**, then **Webhooks**, and select **Add webhook**. Paste the Customer.io URL into **Endpoint URL**, enter a signing secret, and tick only `message.received` under **Events**. See [Webhooks](https://knowledge.flowella.io/settings/webhooks.md).

**3. Send sample data**

Send yourself a WhatsApp template from Flowella and reply to it, so Customer.io receives a real `message.received` event to build the workflow from. Each event looks like this:

```json theme={null}
{
  "id": "evt_3f2b8c1d9a7e4f60b5c2d8e1a4f7b9c3",
  "type": "message.received",
  "created_at": "2026-10-21T09:30:00.000Z",
  "payload_version": "1",
  "data": {
    "messageId": "clxxxxxxxxxxxxxxxxxxxxxxxx",
    "conversationId": "clxxxxxxxxxxxxxxxxxxxxxxxx",
    "contactId": "clxxxxxxxxxxxxxxxxxxxxxxxx",
    "direction": "INBOUND",
    "type": "TEXT",
    "metaMessageId": "wamid.HBgLMTU1NTEyMzQ1NjcVAgASGBQzQUY"
  }
}
```

**Send test event** in Flowella sends a `webhook.test` event, which has no contact, so filter it out in the next step.

**4. Keep only real inbound messages**

Add a **True/False Branch** that continues only when the trigger's `type` is `message.received`.

**5. Look up the contact in Flowella**

The event carries IDs, not the phone number. Add a **Send and receive data** action with the method `GET`, the URL `https://api.flowella.io/v1/contacts/{{trigger.data.contactId}}` and the same `Authorization` header as before. The response has the contact's `id`, `phone`, `waId`, `name`, `email`, `externalId`, `isOptedOut`, `createdAt` and `updatedAt`.

To store the message itself, add another request to `GET /v1/messages/{{trigger.data.messageId}}`. Its `body` field holds the message text and can be empty.

**6. Update the person**

Add a **Create or Update Person** action. Choose the identifier your workspace uses for people, and set only the attributes you need, such as the time of the last WhatsApp reply. See Customer.io's guide to [Create or update person](https://docs.customer.io/journeys/create-update-person/).

> [!WARNING]
>
> In a webhook-triggered automation, **Create or Update Person** creates a new person when no one matches the identifier. To avoid creating people you don't want, add an action condition so the step runs only when the identifier value is present, and test with a number that isn't in your workspace.

### Security of the inbound webhook

Flowella signs every request with the `X-Flowella-Signature` header. A Customer.io webhook-triggered automation can't check that signature, so anyone who has the Customer.io webhook URL can post data to it. Keep the URL private, and don't use the inbound sync to change consent or billing attributes. See [Verify signatures](https://knowledge.flowella.io/api-reference/webhooks.md#verifying-signatures) if you route events through your own endpoint first.

### Opt-outs

When a contact sends `STOP` or another opt-out keyword, Flowella records the opt-out on that WhatsApp number and blocks every later send to them from it, including sends from Customer.io. See [Opt-outs](https://knowledge.flowella.io/app/opt-outs.md).

To copy opt-outs into Customer.io, don't rely on the `optout.created` event: it doesn't fire for a contact's first opt-out on a number at the moment. List opt-outs with `GET /v1/opt-outs` instead, which accepts `createdAfter` and `phone` filters.

## Troubleshooting

| Symptom | Cause | Fix |
| - | - | - |
| `401` with `Invalid or expired API key` | The key was revoked or replaced, or it was pasted incorrectly. | Create a new key and update the `Authorization` header. See [API keys](https://knowledge.flowella.io/settings/api-keys.md). |
| **Could not load API keys** in Flowella | Only Owners and Admins can manage API keys. | Ask an Owner or Admin to create the key. |
| `401` with `Missing bearer token` | The request went to `https://app.flowella.io/api/v1`, which redirects, and the header was dropped. | Use `https://api.flowella.io/v1`. |
| A test returns `202` but nothing arrives | The body or `Idempotency-Key` matched an earlier send within 7 days, or the same template went to the same number within 15 seconds. | Use a new `Idempotency-Key` value for each test. |
| Only the first person in a campaign received the message | A fixed `Idempotency-Key` value was left on the action. | Remove the header, or make its value unique for each person. |
| The send shows a failure on the **Statistics** tab | Meta couldn't deliver the message, for example because the number isn't on WhatsApp. | Read the reason in the **Details** column. See [Meta error codes](https://knowledge.flowella.io/troubleshooting/meta-error-codes.md). |
| **Invalid phone number.** on the **Statistics** tab | The phone attribute is empty or not in international format. | Store numbers with the country code, such as `+447700900123`. |
| Customer.io receives no inbound events | The Flowella webhook is inactive or `message.received` isn't ticked. | Check the webhook's **Delivery log** in Flowella. See [Webhooks](https://knowledge.flowella.io/settings/webhooks.md#delivery-log). |

## Related

> [card] [API introduction](https://knowledge.flowella.io/api-reference/introduction.md)
>
> Authentication, idempotency, rate limits and errors.

> [card] [Webhooks reference](https://knowledge.flowella.io/api-reference/webhooks.md)
>
> Event types, request bodies and signatures.

> [card] [Template variables](https://knowledge.flowella.io/app/template-variables.md)
>
> Variables, samples and fallback values.

> [card] [Bulk send](https://knowledge.flowella.io/app/bulk-send.md)
>
> Send a template to a list from the Flowella app instead.


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