Skip to content
iMessage APIs
Operations10 min read

Building your first iMessage automation: a walkthrough

The technical part takes an afternoon. The decisions around it take longer and matter more — so make them first.

Sending your first programmatic iMessage is genuinely a one-request job. What takes the time is everything around it: who answers replies, what happens on an opt-out, and how you will know whether any of it worked. Do those first and the code is trivial.

Before you write anything

Five decisions

  • Which single play are you building? One triggered message. Not a programme. Pick from the use cases.
  • Who watches replies, and how fast? If the answer is nobody, do not build a message that invites one.
  • What is the opt-out path? Including plain-English requests, not just STOP.
  • Where does consent live? You need a per-contact record of when and how it was given.
  • What is the one number that decides success? Name it now, before you have data to rationalise with.

The third and fourth are not optional and not technical. The compliance guide covers what is legally required versus merely sensible.

Step 1: sandbox account

Both providers covered here offer one free. Sendblue's gives you shared numbers and up to 10 verified contacts; Miss Blue's is API-only with a Message Center preview and no live number. Either is enough to build against. Compare them properly before you pick, but for a first build the difference is small.

Send yourself a message before you write any integration code. It takes two minutes and confirms the credentials work, which eliminates the most common source of confusion later.

Step 2: the send, with tagging built in

Do not write the tagging as a separate step you will remember to do. Put it in the function that builds the message, so it cannot be forgotten.

javascript
function taggedLink(url, campaign, extra = {}) {
  const link = new URL(url);
  link.searchParams.set("utm_source", "imessage");
  link.searchParams.set("utm_medium", "messaging");
  link.searchParams.set("utm_campaign", campaign);
  for (const [key, value] of Object.entries(extra)) {
    link.searchParams.set(`utm_${key}`, value);
  }
  return link.toString();
}

export async function sendReminder(appointment) {
  const link = taggedLink(
    "https://ridgedental.co/visit",
    "appointment-reminder",
  );

  const response = await fetch("https://api.sendblue.co/api/send-message", {
    method: "POST",
    headers: {
      "sb-api-key-id": process.env.SENDBLUE_KEY_ID,
      "sb-api-secret-key": process.env.SENDBLUE_SECRET,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      number: appointment.phone,
      content:
        `Hi ${appointment.firstName} — Ridge Dental. You're booked ` +
        `${appointment.when} for a cleaning. Reply C to confirm or R to move it. ` +
        `Directions: ${link}`,
      status_callback: "https://ridgedental.co/webhooks/message-status",
    }),
  });

  if (!response.ok) {
    // Log and retry later. A failed reminder is a likely no-show —
    // it deserves an alert, not a swallowed error.
    throw new Error(`Send failed: ${response.status}`);
  }

  return response.json();
}

Endpoint and headers follow Sendblue's public API documentation. Credentials come from environment variables — an API key in client-shipped code is a key you have given away.

Step 3: handle the reply

This is the step people skip, and it is the one that makes the difference between a broadcast and a conversation. Your webhook endpoint needs to do three things in order.

javascript
export async function POST(request) {
  const event = await request.json();
  const text = (event.content ?? "").trim().toLowerCase();

  // 1. Opt-outs first, and generously. A human reading this thread
  //    would understand all of these as "stop". So should you.
  if (/^(stop|unsubscribe|cancel|quit)$/.test(text) ||
      /stop (texting|messaging)|take me off|remove me/.test(text)) {
    await suppressContact(event.from_number);   // your system
    await suppressWithProvider(event.from_number);
    return Response.json({ ok: true });
  }

  // 2. Structured replies you asked for.
  if (text === "c") {
    await confirmAppointment(event.from_number);
    await reply(event.from_number, "Confirmed 👍 See you then.");
    return Response.json({ ok: true });
  }

  if (text === "r") {
    await flagForReschedule(event.from_number);
    await reply(event.from_number, "No problem — what day works better?");
    return Response.json({ ok: true });
  }

  // 3. Anything else is a real person asking a real question.
  //    Route it to a human. Do not guess.
  await notifyTeam(event);
  return Response.json({ ok: true });
}

Opt-out handling comes first, before any business logic. It is the one branch that must never fail.

Suppress in both places

The provider's suppression list does not update your CRM, and your CRM does not update theirs. An opt-out that only lands in one of them will eventually send again — which is the single most common compliance failure in small-business messaging.

Step 4: prove it works before it matters

Run the whole path with your own phone number as the recipient. Specifically test the things that break quietly:

  1. Tap the link on a real phone and confirm the UTM parameters survive every redirect. Redirects dropping query strings is the most common silent attribution failure.
  2. Send stop and confirm you are suppressed in both systems.
  3. Send a free-text question and confirm a human is actually notified.
  4. Check the message renders correctly — no merge-field artefacts, no double spaces where a name should be.
  5. Read it an hour later, cold, as if you were the customer.

Step 5: launch small

Run it on a slice of your list — a week of appointments, not a quarter's. Compare against whatever you send today. Watch the delivered-to-read gap and the outcome number you named at the start.

If the outcome number does not move in six weeks, the play is wrong, not the channel. Read receipts will tell you whether it is a timing problem or a copy problem, which is the difference between two very different fixes.

What to build second

Not a second play. Make the first one good: tighten the timing, cut the frequency, fix the ask. A business running one excellent triggered message beats one running six mediocre ones, and the second play is far easier once the plumbing — consent, suppression, tagging, reply routing — already exists.

implementationAPIwebhooks