The Lithic Java SDK provides convenient access to the Lithic REST API from applications written in Java.
The Lithic Java SDK is similar to the Lithic Kotlin SDK but with minor differences that make it more ergonomic for use in Java, such as Optional
instead of nullable values, Stream
instead of Sequence
, and CompletableFuture
instead of suspend functions.
The REST API documentation can be found on docs.lithic.com.
implementation("com.lithic.api:lithic-java:0.76.0")
<dependency>
<groupId>com.lithic.api</groupId>
<artifactId>lithic-java</artifactId>
<version>0.76.0</version>
</dependency>
This library requires Java 8 or later.
Use LithicOkHttpClient.builder()
to configure the client. At a minimum you need to set .apiKey()
:
import com.lithic.api.client.LithicClient;
import com.lithic.api.client.okhttp.LithicOkHttpClient;
LithicClient client = LithicOkHttpClient.builder()
.apiKey("My Lithic API Key")
.build();
Alternately, set the environment with LITHIC_API_KEY
or LITHIC_WEBHOOK_SECRET
, and use LithicOkHttpClient.fromEnv()
to read from the environment.
import com.lithic.api.client.LithicClient;
import com.lithic.api.client.okhttp.LithicOkHttpClient;
LithicClient client = LithicOkHttpClient.fromEnv();
// Note: you can also call fromEnv() from the client builder, for example if you need to set additional properties
LithicClient client = LithicOkHttpClient.builder()
.fromEnv()
// ... set properties on the builder
.build();
Property | Environment variable | Required | Default value |
---|---|---|---|
apiKey | LITHIC_API_KEY |
true | — |
webhookSecret | LITHIC_WEBHOOK_SECRET |
false | — |
Read the documentation for more configuration options.
To create a new card, first use the CardCreateParams
builder to specify attributes, then pass that to the create
method of the cards
service.
import com.lithic.api.models.Card;
import com.lithic.api.models.CardCreateParams;
CardCreateParams params = CardCreateParams.builder()
.type(CardCreateParams.Type.SINGLE_USE)
.build();
Card card = client.cards().create(params);
The Lithic API provides a list
method to get a paginated list of cards. You can retrieve the first page by:
import com.lithic.api.models.Card;
import com.lithic.api.models.CardListPage;
CardListPage page = client.cards().list();
for (Card card : page.data()) {
System.out.println(card);
}
Use the CardListParams
builder to set parameters:
import com.lithic.api.models.CardListPage;
import com.lithic.api.models.CardListParams;
import java.time.OffsetDateTime;
CardListParams params = CardListParams.builder()
.accountToken("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e")
.begin(OffsetDateTime.parse("2019-12-27T18:11:19.117Z"))
.end(OffsetDateTime.parse("2019-12-27T18:11:19.117Z"))
.endingBefore("ending_before")
.pageSize(1L)
.startingAfter("starting_after")
.state(CardListParams.State.CLOSED)
.build();
CardListPage page1 = client.cards().list(params);
// Using the `from` method of the builder you can reuse previous params values:
CardListPage page2 = client.cards().list(CardListParams.builder()
.from(params)
.build());
// Or easily get params for the next page by using the helper `getNextPageParams`:
CardListPage page3 = client.cards().list(params.getNextPageParams(page2));
See Pagination below for more information on transparently working with lists of objects without worrying about fetching each page.
The Lithic SDK generates wrapper classes for all enum properties in the API. You can read and write these values directly using the static instances of the class:
// Read an enum property
if (card.state().equals(Card.State.CLOSED)) {
// ...
}
// Write an enum property
card.builder().state(Card.State.CLOSED).build();
Over time, the Lithic API may add new values to the property that are not yet represented by the enum type in
this SDK. If an unrecognized value is found, the enum is set to a special sentinel value _UNKNOWN
and you can use asString
to read the string that was received:
switch (card.state().value()) {
case Card.State.Value.CLOSED:
// ... handle recognized enum values
break;
...
case Card.State.Value._UNKNOWN:
String cardState = card.state().asString();
// ... handle unrecognized enum value as string
break;
}
To write an unrecognized enum value, pass a string to the wrapper class's of
constructor method:
Card.builder().state(State.of("NEW_STATE")).build()
To make a request to the Lithic API, you generally build an instance of the appropriate Params
class.
See Undocumented request params for how to send arbitrary parameters.
When receiving a response, the Lithic Java SDK will deserialize it into instances of the typed model classes. In rare cases, the API may return a response property that doesn't match the expected Java type. If you directly access the mistaken property, the SDK will throw an unchecked LithicInvalidDataException
at runtime. If you would prefer to check in advance that that response is completely well-typed, call .validate()
on the returned model.
import com.lithic.api.models.Card;
Card card = client.cards().create().validate();
Model properties that are optional or allow a null value are represented as Optional
. The empty case can represent either that the field was provided as null, or that it was simply not present.
// Card.cvv() returns Optional<String>
card.cvv().isPresent(); // false;
In rare cases, you may want to access the underlying JSON value for a response property rather than using the typed version provided by this SDK. Each model property has a corresponding JSON version, with an underscore before the method name, which returns a JsonField
value.
import com.lithic.api.core.JsonField;
import java.util.Optional;
JsonField field = responseObj._field();
if (field.isMissing()) {
// Value was not specified in the JSON response
} else if (field.isNull()) {
// Value was provided as a literal null
} else {
// See if value was provided as a string
Optional<String> jsonString = field.asString();
// If the value given by the API did not match the shape that the SDK expects
// you can deserialise into a custom type
MyClass myObj = responseObj._field().asUnknown().orElseThrow().convert(MyClass.class);
}
Sometimes, the server response may include additional properties that are not yet available in this library's types. You can access them using the model's _additionalProperties
method:
import com.lithic.api.core.JsonValue;
JsonValue secret = address._additionalProperties().get("secret_field");
For methods that return a paginated list of results, this library provides convenient ways access the results either one page at a time, or item-by-item across all pages.
To iterate through all results across all pages, you can use autoPager
, which automatically handles fetching more pages for you:
import com.lithic.api.models.Card;
import com.lithic.api.models.CardListPage;
// As an Iterable:
CardListPage page = client.cards().list(params);
for (Card card : page.autoPager()) {
System.out.println(card);
};
// As a Stream:
client.cards().list(params).autoPager().stream()
.limit(50)
.forEach(card -> System.out.println(card));
// Using forEach, which returns CompletableFuture<Void>:
asyncClient.cards().list(params).autoPager()
.forEach(card -> System.out.println(card), executor);
If none of the above helpers meet your needs, you can also manually request pages one-by-one. A page of results has a data()
method to fetch the list of objects, as well as top-level response
and other methods to fetch top-level data about the page. It also has methods hasNextPage
, getNextPage
, and getNextPageParams
methods to help with pagination.
import com.lithic.api.models.Card;
import com.lithic.api.models.CardListPage;
CardListPage page = client.cards().list(params);
while (page != null) {
for (Card card : page.data()) {
System.out.println(card);
}
page = page.getNextPage().orElse(null);
}
We provide helper methods for verifying that a webhook request came from Lithic, and not a malicious third party.
You can use lithic.webhooks().verifySignature(body, headers, secret?)
or lithic.webhooks().unwrap(body, headers, secret?)
,
both of which will raise an error if the signature is invalid.
Note that the "body" parameter must be the raw JSON string sent from the server (do not parse it first).
The .unwrap()
method can parse this JSON for you.
This library throws exceptions in a single hierarchy for easy handling:
-
LithicException
- Base exception for all exceptions -
LithicServiceException
- HTTP errors with a well-formed response body we were able to parse. The exception message and the.debuggingRequestId()
will be set by the server.400 BadRequestException 401 AuthenticationException 403 PermissionDeniedException 404 NotFoundException 422 UnprocessableEntityException 429 RateLimitException 5xx InternalServerException others UnexpectedStatusCodeException -
LithicIoException
- I/O networking errors -
LithicInvalidDataException
- any other exceptions on the client side, e.g.:- We failed to serialize the request body
- We failed to parse the response body (has access to response code and body)
Requests that experience certain errors are automatically retried 2 times by default, with a short exponential backoff. Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors will all be retried by default. You can provide a maxRetries
on the client builder to configure this:
import com.lithic.api.client.LithicClient;
import com.lithic.api.client.okhttp.LithicOkHttpClient;
LithicClient client = LithicOkHttpClient.builder()
.fromEnv()
.maxRetries(4)
.build();
Requests time out after 1 minute by default. You can configure this on the client builder:
import com.lithic.api.client.LithicClient;
import com.lithic.api.client.okhttp.LithicOkHttpClient;
import java.time.Duration;
LithicClient client = LithicOkHttpClient.builder()
.fromEnv()
.timeout(Duration.ofSeconds(30))
.build();
Requests can be routed through a proxy. You can configure this on the client builder:
import com.lithic.api.client.LithicClient;
import com.lithic.api.client.okhttp.LithicOkHttpClient;
import java.net.InetSocketAddress;
import java.net.Proxy;
LithicClient client = LithicOkHttpClient.builder()
.fromEnv()
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("example.com", 8080)))
.build();
Requests are made to the production environment by default. You can connect to other environments, like sandbox
, via the client builder:
import com.lithic.api.client.LithicClient;
import com.lithic.api.client.okhttp.LithicOkHttpClient;
LithicClient client = LithicOkHttpClient.builder()
.fromEnv()
.sandbox()
.build();
This library is typed for convenient access to the documented API. If you need to access undocumented params or response properties, the library can still be used.
In Example: creating a resource above, we used the CardCreateParams.builder()
to pass to the create
method of the cards
service.
Sometimes, the API may support other properties that are not yet supported in the Java SDK types. In that case, you can attach them using raw setters:
import com.lithic.api.core.JsonValue;
import com.lithic.api.models.CardCreateParams;
CardCreateParams params = CardCreateParams.builder()
.putAdditionalHeader("Secret-Header", "42")
.putAdditionalQueryParam("secret_query_param", "42")
.putAdditionalBodyProperty("secretProperty", JsonValue.from("42"))
.build();
You can also use the putAdditionalProperty
method on nested headers, query params, or body objects.
To access undocumented response properties, you can use res._additionalProperties()
on a response object to get a map of untyped fields of type Map<String, JsonValue>
. You can then access fields like res._additionalProperties().get("secret_prop").asString()
or use other helpers defined on the JsonValue
class to extract it to a desired type.
We use the standard OkHttp logging interceptor.
You can enable logging by setting the environment variable LITHIC_LOG
to info
.
$ export LITHIC_LOG=info
Or to debug
for more verbose logging.
$ export LITHIC_LOG=debug
This package generally follows SemVer conventions, though certain backwards-incompatible changes may be released as minor versions:
- Changes to library internals which are technically public but not intended or documented for external use. (Please open a GitHub issue to let us know if you are relying on such internals.)
- Changes that we do not expect to impact the vast majority of users in practice.
We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
We are keen for your feedback; please open an issue with questions, bugs, or suggestions.