Skip to main content

Customer.io

Customer.io is a messaging automation platform. It has no built-in SMS channel for AlphaSMS, 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 AlphaSMS API, and how to receive delivery reports back.

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

Before you start

  1. An active AlphaSMS 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). AlphaSMS 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 AlphaSMS 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://alphasms.net/api/json.php
HeaderContent-Type: application/json
large campaigns

This endpoint processes the message while Customer.io waits, which is fine for transactional volumes. If you plan large campaigns, ask support about the asynchronous endpoint — it accepts requests into a queue and answers immediately.

Customer.io adds its own X-CIO-Idempotency-Key and X-CIO-Signature headers automatically. No extra authentication header is required — the AlphaSMS 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 answers with HTTP 200 in all cases and puts the result of every message in the body:

{
"success": true,
"data": [
{ "success": true, "data": { "id": "01HB…", "msg_id": 123456789, "parts": 1 } }
]
}

In the Response section of the webhook action click Add attributes and map:

Journey attributeValue
sms_msg_idresponse.data[0].data.msg_id
sms_partsresponse.data[0].data.parts
sms_errorresponse.data[0].error
attention

A rejected message also returns HTTP 200, so Customer.io counts it as a success and never retries. Store response.data[0].success in a journey attribute and branch on it if you need alerting or a fallback channel.

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