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 Sep 11, 2026
Last updated: September 2026. Package versions verified against Maven Central on 2026-09-11.
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.
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.
Twilio's Programmable Messaging API sends and receives SMS and MMS, reports delivery status through webhooks, and schedules sends.
What you need first
Add the dependency
<dependency><groupId>com.twilio.sdk</groupId><artifactId>twilio</artifactId><version>13.0.1</version></dependency>
Or with Gradle:
implementation("com.twilio.sdk:twilio:13.0.1")
Verified against com.twilio.sdk:twilio 13.0.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.
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
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.
Both options above solve delivery. Here is what usually turns up in the fortnight afterward:
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:
Set it up
Create a free account: the Developer plan includes 10,000 messages a month, and past that it's $0.005 per message, the same rate on every channel. 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>6.5.0</version></dependency>
Or with Gradle:
implementation("com.courier:courier-java:6.5.0")
Verified against com.courier:courier-java 6.5.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 helpSystem.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.
If you are writing this in Cursor, Claude Code, or Codex, connect the agent to Courier directly instead of pasting snippets. It then works from the current SDK shapes rather than the 2022-era API that most training data still contains, which is the difference between Java that compiles and Java that does not.
The MCP server gives the agent typed tool calls. In Claude Code:
claude mcp add --transport http courier https://mcp.courier.com --header api_key:YOUR_API_KEY
Courier Skills adds the practices that are easy to get wrong, such as never batching a one-time passcode:
npx skills add trycourier/courier-skills
Both sit alongside the Courier CLI for scripting and CI. courier.com/agents collects the setup for Cursor, Claude Code, Codex, and VS Code in one place, and Build with AI has the machine-readable docs index an agent can fetch on its own.
Then ask in plain language: "send an SMS when an order ships, and fall back to email if it fails." The agent picks the right primitive, and knows which channels need a recorded opt-in.
This matters for the same reason this guide version-stamps every snippet. An agent working from stale training data will confidently write a send call that no longer exists.
| Twilio | MessageBird | Notification API | |
|---|---|---|---|
| Maven artifact | com.twilio.sdk:twilio 13.0.1 | com.messagebird:messagebird-api 6.4.0 | com.courier:courier-java 6.5.0 |
| Client setup | Twilio.init(sid, token) | new MessageBirdClient(service) | CourierOkHttpClient.fromEnv() |
| Send call | Message.creator(...).create() | client.sendMessage(message) | client.send().message(params) |
| Reads credentials from env | you pass them | you pass them | yes, fromEnv() |
| Typed exceptions per status | partial | generic | yes |
| Other channels | SMS, WhatsApp, voice, email | SMS, WhatsApp, voice | whatever you connect |
| Templates outside your code | no | no | yes |
| Preferences and opt-outs | you build it | you build it | built in |
| Provider failover | n/a | n/a | yes, across providers |
| Best for | first send, widest community | per-country pricing | SMS 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.
FAQ
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.Courier.init(), SendEnhancedRequestBody, and new SendService().sendEnhancedMessage(), which belonged to courier-java 1.x. The current SDK is 4.x: you build a client with CourierOkHttpClient.fromEnv(), construct SendMessageParams through nested builders, and call client.send().message(params). Bump the dependency to 6.5.0 or later and rewrite the call.com.twilio.sdk:twilio at 13.0.1. For MessageBird, com.messagebird:messagebird-api at 6.4.0. For Courier as a notification layer over either one, com.courier:courier-java at 6.5.0. Version numbers matter more than usual here, because all three have had breaking changes since the tutorials that most search results still point at.Twilio.init(System.getenv("TWILIO_ACCOUNT_SID"), System.getenv("TWILIO_AUTH_TOKEN")) keeps them out of your repository, and the Courier SDK goes further with CourierOkHttpClient.fromEnv(), which reads COURIER_API_KEY for you. Committed credentials in a public repository get scraped and used within minutes.UserRecipient with a userId and reference a template, and the platform resolves whether that becomes SMS, email, push, or in-app based on routing and the user's stored preferences. Adding a channel later becomes configuration instead of a second integration.Keep exploring
One API, every channel
Courier gives you one API for email, SMS, push, and chat, with templates, routing, retries, and delivery logs built in.
Last updated Sep 11, 2026. Code samples are illustrative; provider APIs and pricing change over time, so check each provider’s docs before relying on them.