> ## Documentation Index
> Fetch the complete documentation index at: https://www.courier.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Install the Courier skill before writing code: `npx skills add trycourier/courier-skills`. It carries the verified SDK shapes and the rules you cannot get wrong.
> Authenticate every request with `Authorization: Bearer <API_KEY>`. A workspace has multiple environments and each environment has its own API keys; start with Test.
> Templates and journeys can be built in the Courier app or created through the API; either way they live in the workspace and are referenced by ID when you send.
> The hosted MCP server is https://mcp.courier.com. For a briefing on what Courier is and when to use it, read https://www.courier.com/llms.txt.

# Send notifications with Java

> Send email, SMS, push, and in-app notifications from Java in five steps.

Courier is one API for every channel your product notifies through: email, SMS, push, Slack, Microsoft Teams, and an in-app inbox. You send once; Courier renders the template, picks the channel from the user's preferences and your routing, and delivers through the providers you already use. This guide gets a Java app from zero to a delivered notification, then adds channel routing and a multi-step journey. About five minutes.

**What you need**

* A Courier account. [Start free](https://app.courier.com/signup).
* A Test API key from [Settings → API Keys](https://app.courier.com/settings/api-keys). Set it as `COURIER_API_KEY`.
* A user in Courier with an email or phone number. Step 2 creates one through the API.

**Building with an AI agent?** Install the Courier skill and your agent knows the API, the channels, and the patterns on this page.

```bash theme={null}
npx skills add trycourier/courier-skills
```

Setup for Claude Code, Cursor, and Codex, plus the hosted MCP server, is in [Build with AI](/docs/tools/ai-onboarding).

## 1. Install the SDK

```kotlin theme={null}
implementation("com.courier:courier-java:6.5.0")
```

Maven works the same way:

```xml theme={null}
<dependency>
  <groupId>com.courier</groupId>
  <artifactId>courier-java</artifactId>
  <version>6.5.0</version>
</dependency>
```

The SDK is a typed client for the whole API, runs on Java 8 and later, and uses OkHttp for transport. `CourierOkHttpClient.fromEnv()` reads `COURIER_API_KEY` from the environment, so there is nothing to configure for the first send. Build one client and reuse it: each instance holds its own connection and thread pools.

## 2. Create a user

Courier is built around users, not addresses. A user profile holds the email, phone number, push tokens, and chat handles for one person, plus their notification preferences. Once it exists, every send names the user and Courier works out where to reach them. Your code never handles a contact detail again.

```java theme={null}
import com.courier.client.CourierClient;
import com.courier.client.okhttp.CourierOkHttpClient;
import com.courier.core.JsonValue;
import com.courier.models.profiles.ProfileCreateParams;

CourierClient client = CourierOkHttpClient.fromEnv();

client.profiles().create(
    "user_123",
    ProfileCreateParams.builder()
        .profile(
            ProfileCreateParams.Profile.builder()
                .putAdditionalProperty("email", JsonValue.from("ada@example.com"))
                .putAdditionalProperty("phone_number", JsonValue.from("+15555550123"))
                .build())
        .build());
```

`create` merges what you pass and leaves every key you omit untouched, which is what you want for everyday writes. Use `client.profiles().replace(...)` only when you mean to overwrite the whole profile.

## 3. Send a notification

A send names a template and a user, and passes the data specific to this event. The template lives in Design Studio, where it holds the content for every channel and the routing rules, so a copy change or a new channel never needs a deploy. The idempotency key means a retried request returns the original response instead of sending twice.

```java theme={null}
import com.courier.models.UserRecipient;
import com.courier.models.send.SendMessageParams;
import com.courier.models.send.SendMessageResponse;

SendMessageResponse response = client.send().message(
    SendMessageParams.builder()
        .idempotencyKey("order-confirmed-ORD-456")
        .message(
            SendMessageParams.Message.builder()
                .template("order-confirmation")
                .to(UserRecipient.builder().userId("user_123").build())
                .data(
                    SendMessageParams.Message.Data.builder()
                        .putAdditionalProperty("order_id", JsonValue.from("ORD-456"))
                        .putAdditionalProperty("total", JsonValue.from("$99.99"))
                        .build())
                .build())
        .build());

String requestId = response.requestId();
```

`idempotencyKey` is sent as the `Idempotency-Key` header. Open [Message Logs](https://app.courier.com/logs) and you will see the request, the channel Courier chose, the provider it used, and the delivery status as it updates.

## 4. Route across channels

Routing normally lives in the template, but a send can override it when the code knows something the template does not. `SINGLE` tries channels in order and stops at the first that delivers; `ALL` sends on every listed channel. The user's preferences still apply on top, so a user who has opted out of SMS gets the email, and a user with no phone number on file does too.

```java theme={null}
client.send().message(
    SendMessageParams.builder()
        .message(
            SendMessageParams.Message.builder()
                .template("password-reset")
                .to(UserRecipient.builder().userId("user_123").build())
                .routing(
                    SendMessageParams.Message.Routing.builder()
                        .method(SendMessageParams.Message.Routing.Method.SINGLE)
                        .addChannel("sms")
                        .addChannel("email")
                        .build())
                .data(
                    SendMessageParams.Message.Data.builder()
                        .putAdditionalProperty(
                            "reset_url", JsonValue.from("https://app.example.com/reset/abc"))
                        .build())
                .build())
        .build());
```

## 5. Start a journey

Some notifications are sequences: a welcome email now, a reminder tomorrow if setup is not finished, a different path for team plans. A journey is that sequence built in a visual editor as steps that send, wait, branch on user data, or digest a burst of events into one message. You publish it once, and your code only has to start it. When the sequence changes, the editor changes; the `invoke` call does not.

```java theme={null}
import com.courier.models.journeys.JourneyInvokeParams;
import com.courier.models.journeys.JourneyRunResponse;
import com.courier.models.journeys.JourneysInvokeRequest;
import com.courier.models.journeys.JourneysInvokeResponse;

JourneysInvokeResponse invoked = client.journeys().invoke(
    "new-signup-onboarding",
    JourneyInvokeParams.builder()
        .journeysInvokeRequest(
            JourneysInvokeRequest.builder()
                .userId("user_123")
                .data(
                    JourneysInvokeRequest.Data.builder()
                        .putAdditionalProperty("plan", JsonValue.from("team"))
                        .build())
                .build())
        .build());

JourneyRunResponse run = client.journeys().runs().retrieve(invoked.runId());
run.run().status(); // Optional<String>: PROCESSING, WAITING, PROCESSED, CANCELED, ERROR, THROTTLED, NOT PROCESSED
```

Runs execute asynchronously, so `invoke` returns a `runId` before any notification is sent. Run status is a plain string rather than an enum, because new values have been added before.

## Confirm delivery

A request fans out to one message per recipient and channel, and each message moves through its own lifecycle: enqueued, sent to the provider, delivered, opened, clicked. Look a message up by ID to read where it is.

```java theme={null}
import com.courier.models.messages.MessageRetrieveResponse;

MessageRetrieveResponse message = client.messages().retrieve(messageId);
message.status(); // ENQUEUED, SENT, DELIVERED, OPENED, CLICKED, UNDELIVERABLE, and more
```

For production, subscribe to an [outbound webhook](/docs/platform/workspaces/outbound-webhooks) instead of polling: Courier posts each status change to your endpoint as it happens.

## Next steps

* [Working example](https://github.com/trycourier/courier-samples/tree/main/server/java): this guide as a runnable project.
* [Add an in-app inbox](/docs/platform/inbox/inbox-overview) with the React or web component SDK.
* [Let users set preferences](/docs/platform/preferences/preferences-overview).
* [Send API reference](/docs/api-reference/send/send-a-message): every field.
* [Java SDK reference](/docs/sdk-libraries/java).
