Integrations

Customer.io AI phone calls: add a call step to journeys

Add an AI phone call to a Customer.io journey: the webhook payload, idempotency, the events that come back, and a full welcome-to-deposit chain you can copy.

Vadim Kouznetsov·Oct 23, 2026·12 min read
A desk telephone connected by a neatly coiled cable to a small router on a pale shelf, with a sealed envelope leaning beside it
Key takeaway

Customer.io has no phone channel, but any journey can make a phone call with one Send and receive data webhook action. The journey POSTs the person, with their language, offer code and an external_id, to the campaign's "start a call" URL. Dial Milo calls them in their local calling hours. About a minute after the call, the outcome comes back as person attributes and events (milo_offer_accepted, milo_opted_out and so on), and your journey branches on them. Customer.io's retries and re-fired journeys can't cause a second call: the external_id makes the request idempotent, and opted-out players are never queued.

The integrations that rank for "Customer.io phone calls" mostly describe the idea in a paragraph and a logo. This page is the build: the request your journey sends, what comes back, how to stop duplicates, and a complete chain you can copy. It runs from the welcome email, through a player who registered but didn't deposit, to the call, the "yes", and the SMS with the code.

The examples use an online casino, because that's where this chain gets the most use. The mechanics work the same for any business with a Customer.io workspace and a list that has agreed to phone calls.

How the two halves fit

DirectionMechanismWhat it carries
Customer.io → Dial MiloA webhook action POSTs to the campaign's secret "start a call" URLWho to call, their language, which offer, when, plus any context Milo should know
Dial Milo → Customer.ioTrack API: identify (attributes) and track (events), using your Site ID and Tracking API keyThe call's status and outcome, which script was used, the callback time, opt-outs and safer-gambling flags

Customer.io decides who gets called and what happens next. Dial Milo handles the call itself: pacing, calling hours in each person's time zone, retries, voicemail detection, the conversation and the outcome.

Step 1: connect Customer.io

In the Dial Milo campaign, open the Customer.io card and enter your Site ID and Tracking API key from Customer.io's Workspace settings → API and webhook credentials. Choose the region (US or EU). Dial Milo checks the credentials before saving them, and never shows the key again.

The card also asks for the Player ID column: the field that holds each person's Customer.io identifier. Dial Milo uses that value to find the right person when it writes results back. The examples below send it as player_id, so set the column to player_id.

Step 2: the webhook action that starts a call

Each campaign has its own secret trigger URL, shown on the campaign page. It has the form …/webhooks/campaigns/<token>/calls. You can rotate or revoke the token from the dashboard if it leaks.

In your journey, add a Send and receive data action. Customer.io defaults to POST with a Content-Type: application/json header. Its webhook action docs cover the full options. Use a body like this:

{
  "phone": "{{customer.phone}}",
  "name": "{{customer.first_name}}",
  "player_id": "{{customer.id}}",
  "external_id": "rnd-48h-{{customer.id}}",
  "language": "{{customer.language | default: 'en'}}",
  "offer_code": "WELCOME100",
  "context": {
    "segment": "registered, no deposit 48h",
    "favourite_game": "{{customer.favourite_game | default: ''}}"
  }
}

What each field does:

  • phone must be in international format (+44…). A national number such as 07700 900123 is refused unless the request also says which country it is. Dial Milo won't guess, because 06… is a mobile in both the Netherlands and France.
  • name is what Milo uses when it confirms it's speaking to the right person.
  • player_id is the identifier Dial Milo sends back with the results (see step 1).
  • external_id is your idempotency key (see step 3).
  • language is the language Milo opens the call in, such as en, nl or de. If the player answers in another language, Milo switches.
  • offer_code has to match one of the offers defined on the campaign. Milo only presents offers you wrote, with the exact terms you wrote, so a code the campaign doesn't know is rejected rather than improvised.
  • at (optional) schedules the call for a later moment, such as "at": "2026-10-20T18:00:00Z". The paced dialler still respects calling hours and pauses, so a time outside the player's hours waits for the next allowed slot.
  • context holds any extra details Milo should know. Each key can also be used in the script as a placeholder, such as {favourite_game}. If a value is empty, Milo leaves it out rather than reading out a blank.

Customer.io's docs warn that quotes and line breaks inside attribute values break a JSON body. Use its strip, strip_newlines or escape filters on free-text fields.

You can send one person, a JSON array, or {"players": [...]} with up to 500 people per request. The response comes back immediately with HTTP 202:

{
  "ok": true,
  "accepted": 1,
  "duplicates": 0,
  "rejected": []
}

A rejected row comes back with its index and a reason a person can read. For example:

{
  "ok": true,
  "accepted": 0,
  "duplicates": 0,
  "rejected": [
    { "index": 0, "reason": "offer_code \"WELCOME10\" is not one of the campaign's offers" }
  ]
}

Other reasons include a number that can't be a phone number, a national number with no country, a premium-rate range, and a field the campaign marks as required but the request left out. You can map response.accepted into a journey attribute and branch on it, so a rejected person takes an email-only path instead of waiting for a call.

Step 3: idempotency, because journeys fire twice

Journeys re-fire. A person re-enters a segment, someone edits a live journey, or Customer.io retries after a timeout. Customer.io retries a webhook up to 11 times over about an hour after a timeout (it waits 16 seconds) or a 408, 409, 429 or 5xx response. Without protection, each of those is a potential second phone call to the same player. A duplicate email is merely annoying. A second call within the hour is the kind of thing people report.

Dial Milo has two guards:

  1. external_id: if a call with this external_id was already accepted on the campaign, the request is counted as a duplicate and ignored. Build the ID from the person and the journey step, such as rnd-48h-{{customer.id}}, so it stays the same across retries and re-entries. Avoid a timestamp, which changes every time.
  2. Pending-call check: even without an external_id, a person who already has a call pending or in progress on that campaign isn't queued again.

Customer.io also adds an X-CIO-Idempotency-Key header to webhook requests. Dial Milo deduplicates on the external_id in the body, so put your key there.

Then there are the guards that aren't about duplicates. Anyone who asked Milo to stop calling, or who was flagged for a safer-gambling concern, is on the do-not-call list, and the webhook won't queue a call for them, whatever the journey sends. The campaign's contact cooldown also holds back a person called by another campaign within the last N days. They're scheduled for later, not dropped.

Step 4: what comes back

About 45 seconds after a call ends, once the summary and outcome are ready, Dial Milo updates the person in Customer.io and sends events. Unanswered attempts that will be retried stay quiet. You only hear about the final attempt, or about the attempt where someone answered.

Person attributes (timestamps are Unix seconds):

AttributeExample
milo_last_call_at1761156000
milo_last_call_statusanswered, no_answer, voicemail, busy or failed
milo_last_outcomeoffer_accepted, offer_declined, callback_requested, opted_out, wrong_person, no_decision
milo_last_campaign"RND welcome call"
milo_last_scriptthe A/B script variant the person heard
milo_callback_atwhen they asked to be called back
milo_opted_out_callstrue once they say stop
milo_safer_gambling_flagtrue if a harm concern came up
milo_feedback_score / milo_feedback_topicwhen the call collected feedback

Events: milo_call_completed comes after every reported call. When there's a specific result, one of these follows: milo_offer_accepted, milo_offer_declined, milo_callback_requested, milo_opted_out, milo_wrong_person or milo_safer_gambling_flag.

A milo_offer_accepted event carries the offer as you defined it, so your SMS template doesn't need a lookup table:

{
  "name": "milo_offer_accepted",
  "data": {
    "call_id": 48211,
    "campaign": "RND welcome call",
    "script_variant": "B",
    "offer_code": "WELCOME100",
    "offer_title": "100% first deposit match up to £50",
    "offer_terms": "Min deposit £10. 10x wagering on bonus funds. Expires 7 days after issue.",
    "offer_valid_until": "2026-10-31"
  }
}

milo_call_completed carries the call status, duration, attempt number, language, outcome and a short AI summary of the conversation. That's useful for a CRM note or a team Slack alert.

The values in these examples are illustrative.

The full chain: Customer.io phone calls from welcome pack to first deposit

Here is the journey this integration was built for, in Customer.io terms.

  1. 1

    Trigger: player registers

    The journey starts on your registered event. Send the welcome email straight away, then the welcome pack: how to verify, payment methods, and the welcome offer.

  2. 2

    Wait Until: deposit, or 48 hours

    Add a Wait Until step with two paths. The first is "has performed first_deposit", which exits to your normal onboarding. The second is a max-time path of 48 hours. Customer.io's Wait Until docs note that without a max time a person can wait forever. If both paths match at the same moment, the leftmost path wins, so put the deposit path on the left.

  3. 3

    Filter: eligible for a call

    Check the phone consent attribute for this product (in the UK, per product and per channel), that milo_opted_out_calls is not true, and that your safer-gambling and exclusion flags are clear. People who fail go down an email-only branch.

  4. 4

    Webhook: start the call

    POST the body from step 2, with language and offer_code. Milo calls in the player's language, inside their calling hours, confirms it's them before naming the brand, checks in ("did you get stuck anywhere?") and presents the welcome offer once.

  5. 5

    Wait Until: a Milo event

    Paths for milo_offer_accepted, milo_callback_requested, milo_opted_out, milo_wrong_person and milo_call_completed, ordered from most to least specific, with a max time of 24 hours.

  6. 6

    Branch: act on the outcome

    Accepted: send the SMS and the email with the code, using the offer fields from the event. Callback requested: don't post the person again. Milo has already scheduled the callback for the time the player asked for, so hold other messages until after milo_callback_at. Opted out: set your phone-consent attribute to false and exit the phone path. Wrong person: flag the number for data cleaning and continue by email. No answer after retries: email only.

  7. 7

    Wait Until: deposit, or 72 hours

    If the accepted offer hasn't turned into a deposit within 72 hours, send one reminder SMS or email with the code and its expiry. Then stop.

Dial Milo doesn't send the marketing SMS here. Customer.io does. Your templates, sending numbers, STOP handling and reporting all stay in one place, and the call step only handles the conversation.

Set the journey's conversion goal to first_deposit, and add the same event as an exit condition. Since Customer.io's July 2026 change, exit conditions are set separately from the goal, so a player who deposits mid-journey won't get a call they no longer need unless you add it yourself.

Branching on opt-outs and safer-gambling flags

These two outcomes deserve their own rules, because getting them wrong does real harm.

  • milo_opted_out: the person told Milo to stop calling. Dial Milo has already put them on the do-not-call list, so no campaign will call them again. In Customer.io, also turn off your own phone-consent attribute, so other journeys don't keep trying to send them. Whether email and SMS continue depends on what they opted out of. "Stop calling me" is a phone opt-out, not an unsubscribe from everything. Respect the wider request if they made one.
  • milo_safer_gambling_flag: Milo stopped selling because something in the conversation suggested harm, gave the player support details, and excluded them from future campaigns. The flag event carries a short reason. Route it to your safer-gambling team, and remove the player from every promotional journey, not just the phone path.

Testing before you go live

  1. 1

    Test the call itself

    Use the campaign's Test call so Milo rings your own phone with the real script and offer. You'll hear the opening, the identity check and how the offer sounds.

  2. 2

    Send one person from Customer.io

    Create a test profile with your own number, run it through the journey, and check the 202 response in the webhook's Sent tab.

  3. 3

    Fire the same request twice

    Retry the webhook manually from the Sent tab. The response should show "duplicates": 1, and your phone shouldn't ring twice.

  4. 4

    Answer, accept, and watch the events

    Accept the offer on the test call. Within a minute or two of hanging up, milo_call_completed and milo_offer_accepted should appear on the test profile, and the SMS branch should fire.

  5. 5

    Say stop

    On a second test, ask Milo to stop calling. Check that milo_opted_out arrives and that a third webhook request for that number doesn't produce a call.

Costs and pacing

Customer.io can send a whole segment in seconds. Dial Milo doesn't call them all at once. The campaign's maximum concurrent calls, calling hours, retries and daily budget decide the pace, and anyone outside their hours waits in the queue. Set a daily and a total budget on the campaign before the journey goes live. You'll get a forecast and email alerts as it spends.

Each call costs the phone line for the exact number dialled plus 9.57¢ per minute of AI talk time. That's roughly 14–20¢ a minute all-in for UK, US and Dutch mobiles, and calls that don't connect have no minute charge. You can also post deposits back to the campaign, so its results show deposits, not just acceptances, and the A/B test can pick the script that brings in money rather than polite yeses.

For the segment strategy behind this chain, read converting registered-no-deposit players. For the wider retention programme, start with player reactivation. For the consent rules that decide who can go down the phone branch, see AI phone call laws. And if you're comparing this with other outbound approaches, the AI outbound calling guide covers the rest.

Frequently asked questions

Can Customer.io make phone calls?
Not natively. Customer.io sends email, SMS, push and in-app messages, and it can call any HTTP endpoint from a journey with a webhook action. That webhook is how you add a phone step: the journey posts the person to an AI calling service, which makes the call and reports the outcome back to Customer.io as attributes and events your journey can branch on.
What happens if Customer.io retries the webhook?
Customer.io retries a webhook up to 11 times over about an hour after a timeout or a 408, 409, 429 or 5xx response. Dial Milo ignores the repeats if you send an external_id, and it never queues a person who already has a call pending or in progress on that campaign. A retry or a re-fired journey cannot produce a second call.
Which events does Dial Milo send back to Customer.io?
milo_call_completed after every finished call that has a result, plus one specific event when there is one: milo_offer_accepted, milo_offer_declined, milo_callback_requested, milo_opted_out, milo_wrong_person or milo_safer_gambling_flag. It also updates person attributes such as milo_last_call_status, milo_last_outcome, milo_last_campaign, milo_last_script, milo_callback_at and milo_opted_out_calls.
Does Dial Milo send the SMS with the bonus code?
No. In this chain Customer.io sends the SMS and the email, triggered by the milo_offer_accepted event. That keeps your message templates, sending numbers, unsubscribe handling and reporting in one place, and the call step only does the conversation.
Will a player who opted out still be called if the journey sends them?
No. Anyone who asked Milo to stop calling, or who was flagged for a safer-gambling concern, is on the do-not-call list, and the webhook will not queue a call for them. Branch on milo_opted_out in Customer.io as well, so the journey stops sending them to the call step in the first place.
Can I use Braze or HubSpot instead of Customer.io?
Any CRM or journey tool that can send a webhook, such as Braze, Optimove, HubSpot workflows or an in-house system, can start calls with the same request. Customer.io is the one where results flow back automatically as person attributes and events.

Never miss a call. Never forget to call back.

From about 12¢ a minute. Outbound calls nobody answers are free. $1 credit to try it.

Already have an account? Sign in