Skip to main content

Customer.io

Customer.io is a messaging automation platform. It has no built-in SMS channel for Interconnect-Solutions, but every Customer.io plan can call an external API with the Send and receive data action (also available as the Webhook channel in broadcasts).

This guide shows how to send SMS from a Customer.io campaign, broadcast or transactional message through the Interconnect-Solutions API, and how to receive delivery reports back.

DirectionHow it works
Customer.io → Interconnect-SolutionsPOST webhook action with a JSON body where type is sms
Interconnect-Solutions → your systemDelivery report to the URL you pass in the hook parameter

Before you start

  1. An active Interconnect-Solutions account with Activate API enabled — see API settings.
  2. An API key (step 1 below).
  3. A sender name — the alphanumeric signature your subscribers see instead of a phone number (3 to 11 Latin letters and digits). Interconnect-Solutions does not use a "from" phone number.
  4. Customer.io profiles that contain a phone attribute in international format (for example +380971234567 or 380971234567).

Step 1. Create an API key

In the Interconnect-Solutions cabinet open Settings → API. The table lists your keys with their creation date, expiration date, state, comment and IP whitelist.

image1

Customer.io - photo 1

Click ADD, type a comment (for example Customer.io), tick Active and press EXECUTE.

image2

Customer.io - photo 2

The full key is displayed once, right after it is created. Copy it now — afterwards the table shows only the first characters of the key.

image3

Customer.io - photo 3

attention

Leave IP whitelisting empty for a key used by Customer.io. Customer.io sends webhooks from a large, changing pool of egress addresses, so a fixed IP list will start rejecting your traffic with Access denied. Use a dedicated key for Customer.io instead, so that you can revoke it without touching your other integrations.

Step 2. Choose the sender name

The sender name is passed with every message in sms_signature, so you can use a different one per campaign — a brand name for marketing, a product name for transactional alerts. Names are handled dynamically: you are not limited to a preset list, and a new name starts working as soon as you send with it.

The only restrictions come from the destination network: some operators and countries accept alphanumeric senders only after registration, and may reject or replace an unknown name. Ask support to confirm the destinations your campaigns target.

The names already used on the account are listed in the cabinet — see Sender ID.

Step 3. Add the webhook action in Customer.io

  • Campaign / journey — open the workflow, drag in the Send and receive data block and click Add Request.
  • Broadcast — on the Content step choose the Webhook channel and click Add content.

image4

Customer.io - photo 4

Configure the request:

FieldValue
MethodPOST
Request URLhttps://api-async.interconnect.solutions/v1/json
HeaderContent-Type: application/json
attention

Use this address for every message sent from Customer.io. The endpoint is asynchronous: it accepts the request into a queue and answers immediately, which is exactly what campaign traffic needs — Customer.io fires one webhook per profile, and a campaign easily produces hundreds of parallel requests.

Customer.io adds its own X-CIO-Idempotency-Key and X-CIO-Signature headers automatically. No extra authentication header is required — the Interconnect-Solutions API is authenticated by the auth field inside the request body.

Step 4. Build the request body

Paste the payload below into the body editor and replace YOUR_API_KEY and YOUR_SENDER_ID. The right-hand Preview panel renders the Liquid against a sample profile, so you can see the exact JSON that will be sent.

image5

Customer.io - photo 5

{% capture sms_text %}Hi {{ customer.first_name | default: 'there' }}, your order is on the way.{% endcapture %}
{
"auth": "YOUR_API_KEY",
"data": [
{
"type": "sms",
"id": "{{delivery_id}}",
"phone": "{{ customer.phone | default: '' | remove: '+' | remove: ' ' | remove: '-' }}",
"sms_signature": "YOUR_SENDER_ID",
"sms_message": {{ sms_text | strip_newlines | json }},
"hook": "https://your-app.example.com/dlr"
}
]
}
ParameterRequiredDescription
authyesYour API key
typeyessms. Other values enable Viber, RCS, WhatsApp and multichannel sending
idnoYour own message identifier, returned in every delivery report. {{delivery_id}} is the Customer.io identifier of this exact message instance
phoneyesRecipient in international format, digits only
sms_signatureyesSender name
sms_messageyesMessage text
hooknoURL that will receive delivery reports for this message
sms_lifetimenoValidity period in seconds, from 60 to 259200 (3 days)
short_linknotrue shortens and tracks links in the text (according to the tariff)
unsubscribe_linknotrue appends an unsubscribe link (according to the tariff)

The full parameter list is documented in Send SMS.

Liquid notes
  • {{delivery_id}} is empty in the composer preview (it shows unsent) and is filled in at send time. Keep it — the value must be unique for every message: a repeated id is treated as a duplicate and the message is not sent.
  • Wrap the text in a capture block and output it with the json filter. The filter adds the surrounding quotes and escapes quotes, backslashes and control characters, so emojis, apostrophes and line breaks in customer data cannot break the JSON.
  • Do not use the escape filter on the message text — in Customer.io it percent-encodes the string (@ becomes %40), and your subscribers will receive the encoded text.
  • Always add | default: '' to attributes that may be missing. Customer.io treats an undefined variable as a composer error (undefined variable: customer.phone) and the request is sent with a broken body, which the API rejects.
  • {{event.*}} variables exist only in event-triggered campaigns. In a broadcast or a segment-triggered campaign they raise the same undefined-variable error.
  • Send one message per profile. If you need to send to several numbers at once, add more objects to the data array.

Step 5. Read the response

The endpoint confirms that the batch was queued and returns the identifier of the request:

{
"request_id": "cf-ray-1234567890-ABC",
"success": true
}

Per-message identifiers are not part of this response — the message is still on its way to the gateway. The request_id is repeated in every delivery report, so it is worth storing. In the Response section of the webhook action click Add attributes and map:

Journey attributeValue
sms_request_idresponse.request_id
sms_queuedresponse.success

Problems with the request itself are reported with real HTTP status codes:

CodeMeaning
400Invalid JSON, or an empty / malformed data array
401auth missing, empty or not accepted
405Method other than POST
413Request body too large
415Content-Type is not application/json
503Queue temporarily unavailable

Customer.io retries 408, 409, 429 and 5xx responses up to 11 times over about an hour, so a short 503 heals itself. A 400, 401 or 415 is a configuration error and will not be retried — watch for those in the campaign metrics after you go live.

An accepted message can still be rejected when the gateway processes it. The reason appears in the delivery report and in Reports → API in the cabinet:

ReasonMeaning
Error in Alpha-nameThe sender name is not allowed on this route or by this operator
Not enough moneyInsufficient balance
Duplicate IDThe id value was already used by this account — it must be unique
Please enter valid receiver phone numberEmpty or malformed phone
Receiver blacklistedThe number is in your black list or has unsubscribed
SMS is too longThe text exceeds the maximum message length
Operator not supportedNo route to this operator

Step 6. Receive delivery reports

Every message that carries a hook parameter produces a POST request to that URL each time its status changes. This is where the final outcome of a send appears, so configure it before launching a campaign.

{
"id": "01HB…",
"msg_id": 123456789,
"type": "sms",
"status": "DELIVERED",
"updated": "2026-08-10T12:34:56+03:00",
"request_id": "cf-ray-1234567890-ABC"
}
  • id — the value you sent in the request (the Customer.io delivery_id in the payload above), which is how you match a report to a message.
  • msg_id — the identifier assigned by the gateway.
  • updated — the moment the status changed, YYYY-MM-DDThh:mm:ss±hh:mm.
  • request_id — the identifier of the request that carried this message (asynchronous endpoint).

The request is signed: the X-Signature header contains sha256(json_body + api_key), computed with the same key that sent the message. Reject requests whose signature does not match.

$body = file_get_contents('php://input');
if (!hash_equals(hash('sha256', $body . $apiKey), $_SERVER['HTTP_X_SIGNATURE'] ?? '')) {
http_response_code(403);
exit;
}

Answer with HTTP 200. Statuses are listed in Message statuses; the most common ones are ACCEPTED, QUEUED, DELIVERED, UNDELIVERABLE, EXPIRED, REJECTED.

An account-wide callback URL can also be set in Settings → API → Callback URL for delivery reports; it applies to all messages of the account. Details: Webhook.

Step 7. Test and go live

  1. Click Send test… in the composer and confirm the request. The response is shown in the Preview panel — this is a real request, so a valid key will really send an SMS.
  2. Check the result in the cabinet: Reports → API shows the message, its price and its status.
  3. Switch the action to Send automatically (campaigns) or finish the broadcast wizard.
Volume and audience
  • There is no fixed request-per-second limit. If you plan bursts of tens of thousands of messages, tell support in advance so the throughput of the account can be reviewed.
  • Customer.io aborts a webhook after 16 seconds. Our API answers well inside that window.
  • Add a filter on the phone attribute (for example phone exists) to the trigger or audience of the campaign, so profiles without a number never reach the webhook.

Other channels

The same webhook action can send Viber, RCS, WhatsApp or a voice call, and can chain them as a fallback — for example Viber first and SMS only if Viber was not delivered. Change type and add the channel parameters:

typeResult
viberViber message
viber+smsViber with SMS fallback
rcs+smsRCS with SMS fallback
voiceVoice call
hlrNumber lookup — operator, roaming and portability, without sending a message