3 ways to send SMS with Java

Send SMS from Java with Twilio, MessageBird, or a notification API. Current Maven coordinates and code that compiles, plus how to pick between the three.

Updated Aug 7, 2026

Last updated: August 2026. All Maven coordinates verified against Maven Central on 2026-08-06.

Java has no built-in way to send a text message, so every option here means calling a third-party API. That part is short. What differs is how much of the machinery around the send you end up owning.

This guide covers three options with code that compiles against current SDK versions: Twilio, MessageBird, and a notification API layered over either.

Which option should you pick?

  • Twilio for the shortest path to a working send and the largest body of existing answers when something breaks.
  • MessageBird if you want per-message pricing that varies by country and are comfortable with a smaller Java community.
  • A notification API in front of either one, once SMS is part of a product flow rather than a one-off. That is where templates, per-user opt-outs, retries, and multi-channel routing stop being small problems.

A warning specific to Java: the versions matter. All three SDKs have shipped breaking changes since the tutorials most search results point at, and Java fails at compile time rather than silently. If a snippet does not compile, check the dependency version before you debug the code.

1. Send SMS with Twilio

Twilio's Programmable Messaging API sends and receives SMS and MMS, reports delivery status through webhooks, and schedules sends.

What you need first

  • A Twilio account, trial or paid.
  • Java 8 or newer.
  • An SMS-enabled Twilio number. On a trial account you are limited to a US or Canadian number, and you can only send to numbers you have verified.
  • For US consumer traffic, a registered 10DLC campaign or a verified toll-free number. Missing this is the most common reason working code delivers nothing in production.

Add the dependency

<dependency>
<groupId>com.twilio.sdk</groupId>
<artifactId>twilio</artifactId>
<version>12.1.1</version>
</dependency>

Or with Gradle:

implementation("com.twilio.sdk:twilio:12.1.1")

Verified against com.twilio.sdk:twilio 12.1.1.

Send the message

import com.twilio.Twilio;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;
public class TwilioSms {
public static void main(String[] args) {
Twilio.init(
System.getenv("TWILIO_ACCOUNT_SID"),
System.getenv("TWILIO_AUTH_TOKEN")
);
Message message = Message.creator(
new PhoneNumber(System.getenv("CELL_PHONE_NUMBER")),
new PhoneNumber(System.getenv("TWILIO_PHONE_NUMBER")),
"Your order has shipped."
).create();
System.out.println(message.getSid() + " " + message.getStatus());
}
}

Argument order in Message.creator is to, then from, then body, which is easy to get backwards. Reading both numbers from the environment rather than passing literals keeps credentials and numbers out of your repository.

The status you get back is queued or accepted, not delivered. To learn what actually happened, either fetch the message again by SID or register a status callback webhook.

Where it gets thin: one provider, message copy compiled into your application, and delivery events arriving as raw webhooks you store and interpret yourself.

2. Send SMS with MessageBird

MessageBird bills per message with rates that vary by destination country. Its Java SDK is maintained, which is worth saying because the company's Node.js SDK is not: MessageBird now trades as Bird, and its newer platform SDK is TypeScript-only. For Java, the messagebird-api library remains the supported path.

What you need first

  • A MessageBird account with a verified phone number, which is what unlocks free test credit.
  • Both API keys from the developer section: a test key that validates requests without sending or billing, and a live key.
  • Java 8 or newer.
  • A registered sender ID or number.

Add the dependency

<dependency>
<groupId>com.messagebird</groupId>
<artifactId>messagebird-api</artifactId>
<version>6.4.0</version>
</dependency>

Verified against com.messagebird:messagebird-api 6.4.0.

Send the message

import com.messagebird.MessageBirdClient;
import com.messagebird.MessageBirdService;
import com.messagebird.MessageBirdServiceImpl;
import com.messagebird.objects.Message;
import com.messagebird.objects.MessageResponse;
public class MessageBirdSms {
public static void main(String[] args) {
MessageBirdService service =
new MessageBirdServiceImpl(System.getenv("MESSAGEBIRD_API_KEY"));
MessageBirdClient client = new MessageBirdClient(service);
// Constructor order is originator, body, recipients.
// The originator is your sender ID, not the message text.
Message message = new Message(
"AcmeInc",
"Your order has shipped.",
"+15558675309"
);
try {
MessageResponse response = client.sendMessage(message);
System.out.println(response.getId());
} catch (Exception e) {
System.err.println("Send failed: " + e.getMessage());
}
}
}

The Message constructor takes originator, body, then recipients, in that order. The originator is the sender identity that shows up on the recipient's phone, so it belongs as a sender ID or number, not the message content. recipients also accepts a List if you are sending to several numbers in one call.

sendMessage returns a MessageResponse carrying the message ID. Capture it; discarding the return value throws away your only handle for looking the message up later.

Where it gets thin: same layer as Twilio, so the same gaps. Also a smaller Java community, so unusual failures take longer to diagnose.

3. Send SMS with a notification API

Both options above solve delivery. Here is what usually turns up in the fortnight afterward:

  • Message copy is a string literal in compiled code, so a wording change is a full deploy.
  • Someone replies STOP and you now own an opt-out list and the obligation behind it.
  • The same event needs to reach one user by SMS and another by email.
  • Your provider has a bad hour and there is no second path.
  • Twenty events fire in a minute and one user gets twenty texts.
  • Someone asks how many notifications failed last week and you cannot answer.

A notification API such as Courier sits above your SMS provider and handles those. You keep Twilio or MessageBird delivering, and your Java code stops knowing which one it is.

What the extra layer gives you:

  • Automatic failover between providers when one starts erroring.
  • Journeys for multi-step sequences, with digest and throttling nodes so a burst of events is not a burst of texts.
  • One send endpoint for SMS, email, push, in-app, Slack, and Microsoft Teams.
  • Per-user, per-channel preferences enforced before a send.
  • Templates edited outside your codebase, with localization per user. For a JVM app this is the big one, because it decouples copy changes from your release cycle.
  • One log covering every channel.

Set it up

Create a free account, then connect an SMS provider under Channels using the credentials that provider already gave you. This is the same Twilio or MessageBird account from the sections above; you are not changing who delivers.

Add the dependency

<dependency>
<groupId>com.courier</groupId>
<artifactId>courier-java</artifactId>
<version>4.25.0</version>
</dependency>

Or with Gradle:

implementation("com.courier:courier-java:4.25.0")

Verified against com.courier:courier-java 4.25.0. Do not copy a 1.x snippet; the API is entirely different and will not compile.

Send the message

import com.courier.client.CourierClient;
import com.courier.client.okhttp.CourierOkHttpClient;
import com.courier.models.ElementalContentSugar;
import com.courier.models.UserRecipient;
import com.courier.models.send.SendMessageParams;
public class CourierSms {
public static void main(String[] args) {
// Reads COURIER_API_KEY from the environment.
CourierClient client = CourierOkHttpClient.fromEnv();
SendMessageParams params = SendMessageParams.builder()
.message(SendMessageParams.Message.builder()
.to(UserRecipient.builder()
.phoneNumber("+15558675309")
.build())
.content(ElementalContentSugar.builder()
.title("Order update")
.body("Hi Erika, your order has shipped.")
.build())
.build())
.build();
var response = client.send().message(params);
System.out.println(response.requestId());
}
}

That sends content defined inline. The version you will run in production references a template you published in Courier and addresses the user rather than a raw number:

SendMessageParams params = SendMessageParams.builder()
.message(SendMessageParams.Message.builder()
.to(UserRecipient.builder()
.userId("user-123")
.build())
.template("ORDER_SHIPPED")
.build())
.build();

Nothing in that call names SMS. Routing and the user's stored preferences decide the channel, so moving a notification to push, or sending both, is configuration rather than a recompile.

The SDK throws typed unchecked exceptions rather than returning error codes, so you catch by status:

try {
var response = client.send().message(params);
System.out.println(response.requestId());
} catch (com.courier.errors.RateLimitException e) {
// back off and retry
} catch (com.courier.errors.BadRequestException e) {
// your payload is wrong; retrying will not help
System.err.println(e.getMessage());
}

CourierServiceException is the base for HTTP errors, with subclasses per status code. Retry RateLimitException and InternalServerException; do not retry BadRequestException, which will fail identically every time.

Where it gets thin: one more service in the path, and one more concept before your first send. It is also the wrong tool for two-way SMS conversations, which belong with your provider's inbound APIs.

Comparing the three options

TwilioMessageBirdNotification API
Maven artifactcom.twilio.sdk:twilio 12.1.1com.messagebird:messagebird-api 6.4.0com.courier:courier-java 4.25.0
Client setupTwilio.init(sid, token)new MessageBirdClient(service)CourierOkHttpClient.fromEnv()
Send callMessage.creator(...).create()client.sendMessage(message)client.send().message(params)
Reads credentials from envyou pass themyou pass themyes, fromEnv()
Typed exceptions per statuspartialgenericyes
Other channelsSMS, WhatsApp, voice, emailSMS, WhatsApp, voicewhatever you connect
Templates outside your codenonoyes
Preferences and opt-outsyou build ityou build itbuilt in
Provider failovern/an/ayes, across providers
Best forfirst send, widest communityper-country pricingSMS inside a product flow

If you need one text sent this afternoon, add the Twilio dependency and stop reading. If SMS is becoming a feature of a JVM application, the argument for the notification layer is stronger than in most languages: getting message copy out of compiled code and into templates means wording changes stop requiring a release.

Next steps

FAQ

Frequently asked questions

Add a provider's SDK as a Maven or Gradle dependency, initialize it with credentials from environment variables, and call its send method with a sender ID, a recipient number in E.164 format, and a body. With Twilio that is `Twilio.init(sid, token)` followed by `Message.creator(to, from, body).create()`. Java has no built-in SMS capability, so every route means calling a provider's HTTP API.

One API, every channel

Ship notifications without the boilerplate

Courier gives you one API for email, SMS, push, and chat, with templates, routing, retries, and delivery logs built in.

Last updated Aug 7, 2026. Code samples are illustrative; provider APIs and pricing change over time, so check each provider’s docs before relying on them.