Skip to main content
When a customer cancels your subscription product, you have a narrow window to understand why — and potentially change their mind. This guide shows how to wire up an Atllas inbound webhook so an AI call fires automatically the moment Stripe registers a cancellation, without any manual intervention. We use this exact setup internally at Atllas.
Phone contact is something your customers should opt into explicitly — it’s a better customer experience and keeps you onside with telemarketing rules in most regions. Store that opt-in on your user record and check it at the moment of cancellation; the example code below shows how.

How It Works

  1. A customer clicks cancel in your app, which sets cancel_at_period_end: true on their Stripe subscription
  2. Stripe fires a customer.subscription.updated event to your webhook endpoint
  3. Your server detects the cancellation, checks the customer’s phone-contact consent, and looks up their phone number
  4. If consent is present, your server POSTs to your Atllas inbound webhook with the customer’s details
  5. Atllas calls the customer using your configured campaign script

Prerequisites

  • An Atllas account with AI Calling enabled
  • An inbound webhook in Atllas bound to a campaign (see Custom Webhooks)
  • A Stripe account with webhook events forwarded to your server
  • The cancelling customer’s phone number available at cancellation time (from your own database or stored on the Stripe Customer object)
  • Recorded consent from each customer to receive phone calls from your business, stored somewhere your server can read at the moment of cancellation
Consent needs to come from somewhere you control — typically a checkbox during signup or in your account settings, with the wording stored alongside the timestamp on the user record. A minimal user shape might look like:
type User = {
  id: string
  stripeCustomerId: string
  phoneNumber?: string
  phoneContactConsent?: {
    accepted: boolean
    acceptedAt: string // ISO timestamp
    version: string    // version of the agreement they accepted
  }
}
A few things to get right:
  • Make it explicit. A pre-checked box in a multi-page terms of service does not count as opt-in in most jurisdictions. Use a separate, unchecked checkbox with clear language: “I agree to receive phone calls from [Your Company] about my account, including AI-assisted calls.”
  • Record what they agreed to. Store the wording version and the timestamp so you can prove consent later if challenged.
  • Make it revocable. Give customers a way to opt out from your account settings or by replying to any phone follow-up. Update phoneContactConsent.accepted to false when they do, and never call them again from that point on.
  • Refresh consent periodically. If you change the wording or significantly change how you use the consent (e.g. adding AI calls when previously you only made human calls), re-prompt rather than relying on stale agreement.

Setting Up the Webhook

  1. Go to Integrations in the left sidebar
  2. Click the Inbound button
  3. Select Custom Webhook — this creates an always-on webhook endpoint ready to receive calls
  4. The webhook will appear on your integrations page with its unique URL — copy this URL, you’ll need it in your server code
  5. Click Phone Call Settings on the webhook to configure the campaign: script, voice, voicemail, follow-ups, and any other call behaviour
Write a script focused on gathering feedback — why they cancelled, what would bring them back, and whether there’s anything you can do right now. See Crafting the Script below for guidance.

Handling the Stripe Webhook

Register a customer.subscription.updated listener in your Stripe webhook handler. The key signal is cancel_at_period_end flipping from false to true — that’s the moment a customer requests cancellation.
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
const ATLLAS_WEBHOOK_URL = process.env.ATLLAS_INBOUND_WEBHOOK_URL // https://wh.atll.as/your-slug

// Your existing Stripe webhook endpoint
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
  let event

  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      req.headers['stripe-signature'],
      process.env.STRIPE_WEBHOOK_SECRET,
    )
  } catch (err) {
    return res.status(400).send(`Webhook signature verification failed: ${err.message}`)
  }

  if (event.type === 'customer.subscription.updated') {
    await handleSubscriptionUpdated(event)
  }

  res.json({ received: true })
})

async function handleSubscriptionUpdated(event) {
  const subscription = event.data.object
  const previousAttributes = event.data.previous_attributes ?? {}

  // Only proceed when cancel_at_period_end just turned true
  const isCancellation = subscription.cancel_at_period_end && !previousAttributes.cancel_at_period_end
  if (!isCancellation) return

  // Look up the user record from your own DB so you can check consent
  const user = await db.users.findOne({ stripeCustomerId: subscription.customer })
  if (!user) return

  // CONSENT GATE — never call a customer who hasn't opted in to phone contact.
  // This must be an explicit, recorded opt-in (see Collecting Consent above).
  if (!user.phoneContactConsent?.accepted) {
    console.info(`Skipping cancellation call for ${user.id}: no phone-contact consent`)
    return
  }

  if (!user.phoneNumber) {
    console.info(`Skipping cancellation call for ${user.id}: no phone number on file`)
    return
  }

  // Fetch customer name and email from Stripe for personalisation
  const customer = await stripe.customers.retrieve(subscription.customer)
  if (customer.deleted) return

  await triggerAtllasCall({
    phoneNumber: user.phoneNumber,
    firstName: customer.name?.split(' ')[0] ?? '',
    lastName: customer.name?.split(' ').slice(1).join(' ') ?? '',
    email: customer.email ?? '',
  })
}

async function triggerAtllasCall({ phoneNumber, firstName, lastName, email }) {
  const response = await fetch(ATLLAS_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      type: 'calls.execute',
      data: { phoneNumber, firstName, lastName, email },
    }),
  })

  if (!response.ok) {
    throw new Error(`Atllas webhook returned ${response.status}: ${await response.text()}`)
  }
}
The example above assumes both the phone number and the consent flag live on the same user record in your own database. That’s the recommended setup — consent and phone number need to be checked together at the moment of cancellation, and your DB is the only place you can guarantee an atomic, up-to-date view of both. If you’d rather rely on Stripe to hold the phone number, you can use the customer.phone field instead:
const customer = await stripe.customers.retrieve(subscription.customer)
const phoneNumber = !customer.deleted ? customer.phone ?? null : null
Even if you read the phone number from Stripe, the consent check still has to happen against your own user record. Stripe doesn’t track phone-contact consent for you, and a phone number on file is not the same thing as permission to call it.

Crafting the Script

The campaign script is where the real value is. A few principles that work well for cancellation recovery:
  • Open with context — the AI should acknowledge the cancellation directly rather than pretending it’s a generic check-in
  • Ask open-ended questions — “What prompted the decision?” gets more signal than “Are you sure you want to cancel?”
  • Offer a path back — if your product has a pause option, a discount, or a different plan, the AI can mention it
  • Keep it short — customers who just cancelled are not in the mood for a long call; 2–3 minutes is the target
  • Always respect a “no” — if they’re firm, thank them and end the call gracefully. A bad cancellation experience costs more than a saved subscription

Production-ready script

Here’s a full SDR-style prompt you can drop into the campaign as a starting point.
Two kinds of placeholders appear in the script below. Treat them differently:
  • {firstName} and {lastName} are dynamic — they are filled in automatically per call from the customer’s contact record. Leave these as-is. If you replace them with a literal name, every call will use that name regardless of who’s actually being called.
  • [Your Company], [Your Product], [discounted plan], [end of billing period], [retention period], [competitor name], [team member / specialist], etc. are static — replace these once with your own brand details and offers before deploying.
# Role

You are Alex, a customer success specialist for [Your Company]. You are warm,
conversational, and genuinely curious. You speak in short, natural sentences — never
read like a script and never sound salesy.

Your single job on this call: understand why this customer cancelled, and — only if
the conversation invites it — offer a relevant path back. This is a courtesy call,
not a save call. Listening is the goal; saving is a bonus.

# Context

You are calling {firstName} {lastName}. They cancelled their [Your Product]
subscription earlier today. Their access continues until [end of billing period],
so nothing has been charged to win them back. They are not currently angry — they
just clicked cancel.

# Voice and pacing

- Keep every spoken line short — aim for under 20 words. Long sentences sound robotic over the phone.
- Use natural contractions ("I'd", "you're", "we've").
- Use the customer's first name **only once**, in the opening confirmation. Do not repeat their name later in the call — it sounds salesy and robotic. Use "you" from then on.
- One question at a time. Wait for an answer before asking another.
- If the caller pauses, give them a beat. Do not fill silence by talking.
- Mirror their tone: brisk if they're brisk, warm if they're warm.
- Acknowledge what they said before pivoting — never jump straight to a question.
- Keep the whole call under three minutes unless they keep it going.

# Step 1 — Confirm and open

Say exactly: "Hi, is this {firstName}?"

If they say yes:
"This is Alex from [Your Company]. I saw you cancelled today and wanted to ask quickly what led to that — is now an okay time?"

If they say no, or it's the wrong number:
Apologize briefly and end the call.

If they say it's a bad time:
"Totally understand — I'll let you go. If you'd like to share later, just reply to the cancellation email." Then end the call.

If they say yes, continue to Step 2.

# Step 2 — Discover the reason

Ask: "What prompted the decision to cancel?"

Then listen. Do not pitch, do not offer anything yet. If their answer is short or
vague, ask one — and only one — natural follow-up:

- "Was there a specific moment that pushed you over the edge?"
- "How long had that been bothering you?"
- "If we could fix one thing tomorrow, what would it be?"

From their answer, classify the reason as one of:

1. PRICING — too expensive, value doesn't match cost
2. PRODUCT_GAP — missing feature, doesn't solve their actual problem
3. RELIABILITY — something broke, support didn't resolve it
4. NO_LONGER_NEEDED — situation changed, project ended, no longer relevant
5. COMPETITOR — moving to another product
6. UNUSED — never got around to using it

If you cannot tell, ask one clarifying question. If still unclear, treat it as
NO_LONGER_NEEDED.

# Step 3 — Respond based on the reason

Use the matching response below. Acknowledge first, then offer.

PRICING
"That's fair — price has to match value. We do have a [discounted plan / annual option / pause feature] some folks switch to instead. Want me to have someone reach out with options?"

PRODUCT_GAP
"Got it, that makes sense. Quick one for our product team — was that the main thing missing, or were there others? And if we shipped it, would you come back?"

RELIABILITY
"I'm really sorry that happened. Want me to have a senior support engineer reach out today and take another look? Even if you stay cancelled, I'd like to make sure it's actually fixed."

NO_LONGER_NEEDED
"Totally understandable. Quick question so I can mark this right — is it 'never again', or 'maybe down the road'? We can pause instead of cancel so your data sticks around."

COMPETITOR
"[competitor name] is solid. Out of curiosity — was there one specific thing that tipped it, or more of an overall feel?"

UNUSED
"Got it — sounds like it never quite fit your workflow. If we offered [a free month or two / a quick onboarding call] to set you up properly, worth a shot? Or is it more of a 'wrong product' thing?"

# Step 4 — Close

Always end the call with these three beats, in order:

1. Thank them sincerely: "Really appreciate you taking the time."

2. Confirm what happens next:
   - If they accepted a follow-up: "I'll have [team member / specialist] reach out by [end of day / tomorrow]."
   - If they declined: "Totally respect that. Access stays on until [end of billing period], then it wraps up automatically."

3. Leave the door open: "If anything changes, you're always welcome back — we keep your data on file for [retention period]."

Then say goodbye and end the call.

# Edge cases

If they ask "Is this a real person?" or "Are you AI?":
Say honestly: "I'm an AI calling on behalf of [Your Company]. I can pass you to a
real teammate if you'd prefer." If they want a human, offer to transfer or schedule
a callback.

If they say "I didn't cancel":
"Sorry for the confusion — let me get our billing team to look into it today. What's the best email to reach you?" Then end the call without further questions.

If they sound frustrated or angry at any point:
Drop the recovery angle entirely. Apologize, listen, and offer to escalate.

If they ramble past three minutes and you're still on Step 2:
Gently redirect: "I want to be respectful of your time — okay if someone follows up by email with the rest?"

# Hard rules

- Never argue with a cancellation reason. If they say the price is too high, do not say it isn't.
- Never offer a discount before they've raised price as the issue.
- Never claim the cancellation has been reversed or paused unless the customer explicitly agreed to it on this call.
- Never use the words "retention", "save", "win-back", or "churn" out loud.
- Never read the placeholder text literally — if you cannot personalize, omit the sentence.
- One topic at a time. Do not stack multiple offers in one breath.
The prompt is intentionally verbose. As you collect real call transcripts, prune the branches that don’t get used and tighten the language on the ones that produce the best save rates.

Other Phone Call Settings

The script is what the AI says, but Phone Call Settings on the webhook also controls how the call itself behaves. For cancellation recovery specifically, four settings are worth tuning:

Voicemail

Most cancelling customers won’t pick up — they’re either busy or actively avoiding the call. Configure a short voicemail so the call still produces value when it goes unanswered. What works for this use case:
  • Keep it under 20 seconds. Long voicemails get deleted before they finish.
  • Lead with who you are and why you’re calling, then make the ask soft. Don’t request a callback — most people won’t, and it pressures them.
  • Point them to a low-friction alternative, like replying to your cancellation email or filling out a one-question feedback form.
Example voicemail script:
“Hi , this is Alex from [Your Company]. I noticed you cancelled today and wanted to ask quickly what led to that. No need to call back — if you’d like to share, just reply to the cancellation email and it’ll come straight to me. Thanks again.”

Call Forwarding

Forwarding lets the AI hand the call off to a real person mid-conversation. For cancellation recovery, this is the escape valve that turns a frustrated customer into a saved one. Configure forwarding to your customer success or billing team’s phone number, and trigger it when:
  • The customer asks to speak to a human
  • The customer says they didn’t cancel (likely a billing error — escalate to billing)
  • The customer is clearly upset and the AI can’t de-escalate
  • The customer agrees to a discount or pause and wants to lock it in immediately
Make sure whoever is on the receiving end knows what context to expect — they’ll be picking up mid-conversation about a cancellation.

Follow-ups (SMS / Email)

For calls where the AI promised something — a discount offer, a support engineer reaching out, an email reply — configure a follow-up message that fires after the call so the promise doesn’t fall through the cracks. The customer gets a written record of what was agreed, and your team gets a clean handoff trigger. You can configure all of these under Phone Call Settings on the inbound webhook you created earlier — see Custom Webhooks for the full list of options.

Viewing Results

Every call triggered by the webhook appears under the bound campaign in AI Calling. Each result includes:
  • Full transcript and recording
  • AI summary of the conversation
  • Lead warmth classification (hot if they’re open to returning, cold if firm)
  • Any action items extracted by the AI (e.g. “follow up with discount offer”)
If you want the results delivered back to your own system automatically, configure a Response Webhook on the inbound webhook.
Lead warmth on cancellation calls maps well to win-back potential: hot and warm leads are worth a follow-up from your sales or success team; cold and dead leads are good candidates for an exit survey email instead.