Java SDK
The official Java SDK for Sent provides an enterprise-ready client with full support for synchronous and asynchronous operations. Built for Spring Boot, Jakarta EE, and standalone applications with builder patterns throughout.
Requirements
This library requires Java 8 or later.
Installation
<dependency>
<groupId>dm.sent</groupId>
<artifactId>sent-java</artifactId>
<!-- Use the latest version from https://github.com/sentdm/sent-dm-java/releases -->
<version>x.y.z</version>
</dependency>// Use the latest version from https://github.com/sentdm/sent-dm-java/releases
implementation("dm.sent:sent-java:x.y.z")Quick Start
Initialize the client
import dm.sent.client.SentClient;
import dm.sent.client.okhttp.SentOkHttpClient;
// Configures using the `sent.dmApiKey` system property
// Or configures using the `SENT_DM_API_KEY` environment variable
SentClient client = SentOkHttpClient.fromEnv();Send your first message
import dm.sent.client.SentClient;
import dm.sent.client.okhttp.SentOkHttpClient;
import dm.sent.core.JsonValue;
import dm.sent.models.messages.MessageSendParams;
import dm.sent.models.messages.MessageSendResponse;
SentClient client = SentOkHttpClient.fromEnv();
MessageSendParams params = MessageSendParams.builder()
.addTo("+1234567890")
.addChannel("sms")
.addChannel("whatsapp")
.addChannel("rcs")
.template(MessageSendParams.Template.builder()
.id("7ba7b820-9dad-11d1-80b4-00c04fd430c8")
.name("order_confirmation")
.parameters(MessageSendParams.Template.Parameters.builder()
.putAdditionalProperty("name", JsonValue.from("John Doe"))
.putAdditionalProperty("order_id", JsonValue.from("12345"))
.build())
.build())
.build();
MessageSendResponse response = client.messages().send(params);
System.out.println("Sent: " + response.data().recipients().get().get(0).messageId());
System.out.println("Status: " + response.data().status());Client configuration
Configure the client using system properties or environment variables:
| Setter | System property | Environment variable | Required | Default value |
|---|---|---|---|---|
apiKey | sent.dmApiKey | SENT_DM_API_KEY | true | - |
baseUrl | sent.baseUrl | SENT_BASE_URL | false | "https://api.sent.dm" |
System properties take precedence over environment variables.
import dm.sent.client.SentClient;
import dm.sent.client.okhttp.SentOkHttpClient;
// From environment variables
SentClient client = SentOkHttpClient.fromEnv();
// Or manually configure
SentClient client = SentOkHttpClient.builder()
.apiKey("your_api_key")
.build();
// Or combine both approaches
SentClient client = SentOkHttpClient.builder()
.fromEnv()
.apiKey("overridden_api_key")
.build();Don't create more than one client in the same application. Each client has a connection pool and thread pools, which are more efficient to share between requests.
Send Messages
Send a message
import dm.sent.core.JsonValue;
import dm.sent.models.messages.MessageSendParams;
import dm.sent.models.messages.MessageSendResponse;
MessageSendParams params = MessageSendParams.builder()
.addTo("+1234567890")
.addChannel("sms")
.addChannel("whatsapp")
.addChannel("rcs")
.template(MessageSendParams.Template.builder()
.id("7ba7b820-9dad-11d1-80b4-00c04fd430c8")
.name("order_confirmation")
.parameters(MessageSendParams.Template.Parameters.builder()
.putAdditionalProperty("name", JsonValue.from("John Doe"))
.putAdditionalProperty("order_id", JsonValue.from("12345"))
.build())
.build())
.build();
MessageSendResponse response = client.messages().send(params);
System.out.println("Message ID: " + response.data().recipients().get().get(0).messageId());
System.out.println("Status: " + response.data().status());Sandbox mode
Use sandbox(true) to validate requests without sending real messages:
MessageSendParams params = MessageSendParams.builder()
.addTo("+1234567890")
.template(MessageSendParams.Template.builder()
.id("7ba7b820-9dad-11d1-80b4-00c04fd430c8")
.name("order_confirmation")
.build())
.sandbox(true) // Validates but doesn't send
.build();
MessageSendResponse response = client.messages().send(params);
// Response will have test data
System.out.println("Validation passed: " + response.data().recipients().get().get(0).messageId());Check message status
Retrieve the current status of a sent message. The direction field indicates whether the message is "OUTBOUND" (sent by you) or "INBOUND" (a reply or opt-out keyword received from an end user):
import dm.sent.models.messages.MessageRetrieveStatusResponse;
MessageRetrieveStatusResponse status = client.messages().retrieveStatus("msg-uuid");
System.out.println("Status: " + status.data().status()); // e.g. "DELIVERED"
System.out.println("Channel: " + status.data().channel()); // e.g. "sms"
System.out.println("Direction: " + status.data().direction()); // "OUTBOUND" | "INBOUND"Message activities
Retrieve the full activity log for a message, useful for auditing delivery attempts across carriers:
import dm.sent.models.messages.MessageRetrieveActivitiesResponse;
MessageRetrieveActivitiesResponse activities = client.messages().retrieveActivities("msg-uuid");
activities.data().activities().forEach(activity -> {
System.out.println(activity.timestamp() + ": " + activity.status() + " via " + activity.from());
System.out.println(" Price: " + activity.price());
System.out.println(" Active contact price: " + activity.activeContactPrice());
});Numbers
Look up carrier and line-type information for any phone number before sending:
import dm.sent.models.numbers.NumberLookupResponse;
NumberLookupResponse result = client.numbers().lookup("+12025551234");
System.out.println("Carrier: " + result.data().carrierName());
System.out.println("Line type: " + result.data().lineType()); // "mobile", "landline", "voip"
System.out.println("VoIP: " + result.data().isVoip());Asynchronous execution
The default client is synchronous. To switch to asynchronous execution, call the async() method:
import dm.sent.client.SentClient;
import dm.sent.client.okhttp.SentOkHttpClient;
import dm.sent.models.messages.MessageSendParams;
import dm.sent.models.messages.MessageSendResponse;
import java.util.concurrent.CompletableFuture;
SentClient client = SentOkHttpClient.fromEnv();
MessageSendParams params = MessageSendParams.builder()
.addTo("+1234567890")
.template(MessageSendParams.Template.builder()
.id("7ba7b820-9dad-11d1-80b4-00c04fd430c8")
.build())
.build();
CompletableFuture<MessageSendResponse> future = client.async().messages().send(params);
// Handle result
future.thenAccept(response -> {
System.out.println("Sent: " + response.data().recipients().get().get(0).messageId());
}).exceptionally(throwable -> {
System.err.println("Failed: " + throwable.getMessage());
return null;
});Or create an asynchronous client from the beginning:
import dm.sent.client.SentClientAsync;
import dm.sent.client.okhttp.SentOkHttpClientAsync;
SentClientAsync client = SentOkHttpClientAsync.fromEnv();
CompletableFuture<MessageSendResponse> future = client.messages().send(params);Error handling
When the API returns a non-success status code, a subclass of SentServiceException (itself a subclass of SentException) will be thrown:
| Status | Exception |
|---|---|
| 400 | BadRequestException |
| 401 | UnauthorizedException |
| 403 | PermissionDeniedException |
| 404 | NotFoundException |
| 422 | UnprocessableEntityException |
| 429 | RateLimitException |
| >=500 | InternalServerException |
| others | UnexpectedStatusCodeException |
try {
MessageSendResponse response = client.messages().send(params);
System.out.println("Sent: " + response.data().recipients().get().get(0).messageId());
} catch (NotFoundException e) {
System.err.println("Contact or template not found: " + e.getMessage());
} catch (RateLimitException e) {
System.err.println("Rate limited: " + e.getMessage());
} catch (UnauthorizedException e) {
System.err.println("Authentication failed - check API key");
} catch (SentException e) {
System.err.println("Error: " + e.getMessage());
}Raw responses
To access response headers, status code, or raw body, prefix any HTTP method call with withRawResponse():
import dm.sent.core.http.Headers;
import dm.sent.core.http.HttpResponseFor;
import dm.sent.models.messages.MessageSendResponse;
HttpResponseFor<MessageSendResponse> response = client.messages().withRawResponse().send(params);
int statusCode = response.statusCode();
Headers headers = response.headers();
// Deserialize if needed
MessageSendResponse parsed = response.parse();Contacts
Create and manage contacts:
import dm.sent.models.contacts.ApiResponseOfContact;
import dm.sent.models.contacts.ContactCreateParams;
import dm.sent.models.contacts.ContactDeleteParams;
import dm.sent.models.contacts.ContactListParams;
import dm.sent.models.contacts.ContactListResponse;
import dm.sent.models.contacts.ContactUpdateParams;
import dm.sent.models.webhooks.MutationRequest;
// Create a contact
ContactCreateParams createParams = ContactCreateParams.builder()
.phoneNumber("+1234567890")
.build();
ApiResponseOfContact contact = client.contacts().create(createParams);
System.out.println("Contact ID: " + contact.data().id());
// List contacts
ContactListParams listParams = ContactListParams.builder()
.page(1)
.pageSize(100)
.build();
ContactListResponse contacts = client.contacts().list(listParams);
contacts.data().contacts().forEach(c ->
System.out.println(c.phoneNumber() + " - " + c.availableChannels())
);
// Get a contact
ApiResponseOfContact retrieved = client.contacts().retrieve("contact-uuid");
// Update a contact
ContactUpdateParams updateParams = ContactUpdateParams.builder()
.defaultChannel("whatsapp")
.build();
ApiResponseOfContact updated = client.contacts().update("contact-uuid", updateParams);
// Delete a contact
client.contacts().delete(
ContactDeleteParams.builder()
.id("contact-uuid")
.mutationRequest(MutationRequest.builder().build())
.build()
);Templates
List and retrieve templates:
import dm.sent.models.templates.ApiResponseTemplate;
import dm.sent.models.templates.TemplateListParams;
import dm.sent.models.templates.TemplateListResponse;
// List templates
TemplateListResponse templates = client.templates().list(
TemplateListParams.builder()
.page(1)
.pageSize(100)
.build()
);
templates.data().templates().forEach(template ->
System.out.println(template.name() + " (" + template.status() + "): " + template.id())
);
// Get a specific template
ApiResponseTemplate template = client.templates().retrieve("template-uuid");
System.out.println("Name: " + template.data().name());
System.out.println("Status: " + template.data().status());Framework Integration
A dedicated guide covers client configuration, message sending, verified webhook handling, and sandbox testing:
Client customization
To temporarily use a modified client configuration, while reusing the same connection and thread pools, call withOptions():
import dm.sent.client.SentClient;
SentClient clientWithOptions = client.withOptions(optionsBuilder -> {
optionsBuilder.baseUrl("https://example.com");
optionsBuilder.maxRetries(5);
});The withOptions() method does not affect the original client.
Immutability
Each class in the SDK has an associated builder for constructing it. Each class is immutable once constructed. If the class has an associated builder, then it has a toBuilder() method for making a modified copy.
MessageSendParams params = MessageSendParams.builder()
.addTo("+1234567890")
.template(MessageSendParams.Template.builder()
.id("template-id")
.build())
.build();
// Create a modified copy
MessageSendParams modified = params.toBuilder()
.addTo("+0987654321")
.build();Webhooks
Recommended pattern: Webhooks are the primary way to track message delivery, so don't poll the API. Save the message ID when you send, then update your database as webhook events arrive.
Sent delivers signed POST requests to your endpoint for every status change. Two event types exist:
message: Message status changes (QUEUED,ROUTED,SCHEDULED,SENT,DELIVERED,READ,FAILED,FILTERED,BLOCKED,RECEIVED); each fires as a sub-type (for example,message.delivered,message.filtered). Usemessage.receivedto receive inbound messages from contacts.templates: WhatsApp template approval/rejection
The signing secret (from the Sent Dashboard) has a whsec_ prefix. Strip it and base64-decode the remainder to obtain the raw HMAC key. The signed content is {X-Webhook-ID}.{X-Webhook-Timestamp}.{rawBody} and the signature format is v1,{base64(hmac)}.
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;
@RestController
public class WebhookController {
private final ObjectMapper objectMapper = new ObjectMapper();
@PostMapping("/webhooks/sent")
public ResponseEntity<?> handleWebhook(
@RequestBody byte[] payload, // raw bytes — do NOT use @RequestBody String
@RequestHeader("X-Webhook-ID") String webhookId,
@RequestHeader("X-Webhook-Timestamp") String timestamp,
@RequestHeader("X-Webhook-Signature") String signature
) throws Exception {
// 1. Verify: signed content = "{webhookId}.{timestamp}.{rawBody}"
String secret = System.getenv("SENT_DM_WEBHOOK_SECRET"); // "whsec_abc123..."
String keyBase64 = secret.startsWith("whsec_") ? secret.substring(6) : secret;
byte[] keyBytes = Base64.getDecoder().decode(keyBase64);
String signed = webhookId + "." + timestamp + "." + new String(payload, StandardCharsets.UTF_8);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(keyBytes, "HmacSHA256"));
String expected = "v1," + Base64.getEncoder().encodeToString(
mac.doFinal(signed.getBytes(StandardCharsets.UTF_8))
);
if (!MessageDigest.isEqual(expected.getBytes(), signature.getBytes())) {
return ResponseEntity.status(401).body(Collections.singletonMap("error", "Invalid signature"));
}
// 2. Optional: reject replayed events older than 5 minutes
if (Math.abs(Instant.now().getEpochSecond() - Long.parseLong(timestamp)) > 300) {
return ResponseEntity.status(401).body(Collections.singletonMap("error", "Timestamp too old"));
}
// 3. Handle events — update message status in your own database
Map<String, Object> event = objectMapper.readValue(payload, Map.class);
if ("message".equals(event.get("field"))) {
Map<String, Object> p = (Map<String, Object>) event.get("payload");
// messageRepository.updateStatus((String) p.get("message_id"), (String) p.get("message_status"));
}
// 4. Always return 200 quickly
return ResponseEntity.ok(Collections.singletonMap("received", true));
}
}See the Webhooks reference for the full payload schema and all status values.
Source & Issues
- Releases: GitHub Releases
- GitHub:
sentdm/sent-dm-java - Maven Central: dm.sent:sent-java
- Javadoc: javadoc.io
- Issues: Report a bug
Getting Help
- Documentation: API Reference
- Troubleshooting: Common Issues
- Support: email support@sent.dm with your request ID
Sending messages from an Echo service with the Sent Go SDK
Wire the Sent Go SDK into an Echo service: install, configure the client, send a template message from a handler, verify webhooks, and test with sandbox mode.
Sending messages from Spring Boot with the Sent Java SDK
Wire the Sent Java SDK into a Spring Boot app: install, register a client bean, send messages from a controller, verify webhooks, and test with sandbox mode.