Official Smile ID server-side SDK for Java — V3 APIs.
This repository is under active development. The SDK is not yet published, and the API surface may change without notice.
- Group:
com.smileidentity - Artifact:
usesmileid-java - Requires Java 11 or later
Not yet published to Maven Central. Once published:
Gradle:
implementation("com.smileidentity:usesmileid-java:12.0.0")Maven:
<dependency>
<groupId>com.smileidentity</groupId>
<artifactId>usesmileid-java</artifactId>
<version>12.0.0</version>
</dependency>Until then, build from source with ./gradlew build and use the jar from build/libs/.
Construct one SmileID client with your partner id and API key. The SDK manages authentication for you: it fetches an internal token, caches it and refreshes it when it expires. You never handle tokens yourself.
import com.smileidentity.client.Environment;
import com.smileidentity.client.SmileID;
SmileID smile = SmileID.builder()
.partnerId("1234")
.apiKey(System.getenv("SMILE_API_KEY"))
.environment(Environment.SANDBOX)
.defaultCallbackUrl("https://app.example.com/callback")
.build();Configuration options:
| Option | Default | Notes |
|---|---|---|
partnerId |
required | Numeric string, no leading zeros |
apiKey |
required | Partner API key |
environment |
SANDBOX |
SANDBOX or PRODUCTION |
defaultCallbackUrl |
unset | Used when a call omits callbackUrl; must be https |
baseUrl |
derived | Explicit override; wins over environment; must be https |
timeout |
30 seconds | Per-request total timeout |
maxRetries |
2 | Idempotent operations only |
httpClient |
SDK default | Inject your own OkHttpClient |
Partner ids are displayed zero-padded in the portal (for example 002) but must be passed without the leading zeros ("2").
environment only names the sandbox and production. To reach any other Smile ID host — a development API, for instance — set baseUrl, which wins over environment:
SmileID smile = SmileID.builder()
.partnerId("2")
.apiKey(System.getenv("SMILE_API_KEY"))
.baseUrl("https://your-environment.example.com")
.build();The SDK only talks https. baseUrl must be an absolute https URL with no query or fragment — the builder rejects anything else and there is no insecure override. Callback URLs (defaultCallbackUrl and per-request callbackUrl values) must be https too: the default is checked when you build the client, per-request values are checked before any request is sent.
The client targets the sandbox by default. Select production explicitly:
Environment.SANDBOX→https://testapi.smileidentity.comEnvironment.PRODUCTION→https://api.smileidentity.com
Most verification calls need end-user details and consent:
import com.smileidentity.generated.models.Consent;
import com.smileidentity.generated.models.UserDetails;
import java.time.Instant;
UserDetails user = UserDetails.builder()
.givenNames("Amina Fatou")
.lastName("Clearwater")
.email("amina.clearwater@example.com") // at least one of email or phoneNumber is required
.build();
Consent consent = Consent.granted(Instant.now(), "EN", "https://example.com/privacy");Non-production environments match test identities on given names, last name and email. The identity above is a recognised test identity and resolves to clear; an unrecognised one resolves to block.
Binary inputs (selfies, liveness frames, documents) accept a File, a byte[] or an InputStream via BinaryInput.of(...), with optional .withFilename(...) and .withContentType(...).
import com.smileidentity.generated.models.AcceptedResponse;
import com.smileidentity.generated.models.EnhancedKycParams;
AcceptedResponse accepted = smile.enhancedKyc().verify(EnhancedKycParams.builder()
.country("NG").idType("NIN").idNumber("12345678901")
.userDetails(user)
.consent(consent)
.userId("user_01h8x9y2z3a4b5c6d7e8f9g0h1")
.build());
accepted.getJobId(); // "job_..."
accepted.isAccepted(); // true — normalizes "Accepted" and "accepted"import com.smileidentity.generated.models.DocumentVerificationParams;
import com.smileidentity.helpers.BinaryInput;
import java.io.File;
import java.util.Arrays;
import java.util.List;
List<BinaryInput> livenessFrames = Arrays.asList(
BinaryInput.of(new File("live1.jpg")),
BinaryInput.of(new File("live2.jpg")),
BinaryInput.of(new File("live3.jpg")),
BinaryInput.of(new File("live4.jpg")),
BinaryInput.of(new File("live5.jpg")),
BinaryInput.of(new File("live6.jpg"))); // 6 to 8 frames
AcceptedResponse accepted = smile.documents().verify(DocumentVerificationParams.builder()
.selfieImage(BinaryInput.of(new File("selfie.jpg")))
.livenessImages(livenessFrames)
.document(BinaryInput.of(new File("doc.jpg")))
.country("NG") // idType optional: auto-classified
.userDetails(user)
.consent(consent)
.build());Same shape as document verification, but idType is required:
import com.smileidentity.generated.models.EnhancedDocumentVerificationParams;
AcceptedResponse accepted = smile.documents().verifyEnhanced(
EnhancedDocumentVerificationParams.builder()
.selfieImage(BinaryInput.of(new File("selfie.jpg")))
.livenessImages(livenessFrames)
.document(BinaryInput.of(new File("doc.jpg")))
.country("NG").idType("PASSPORT")
.userDetails(user)
.consent(consent)
.build());import com.smileidentity.generated.models.BiometricKycParams;
AcceptedResponse accepted = smile.biometricKyc().verify(BiometricKycParams.builder()
.selfieImage(BinaryInput.of(new File("selfie.jpg")))
.livenessImages(livenessFrames)
.country("NG").idType("NIN").idNumber("12345678901")
.userDetails(user)
.consent(consent)
.build());import com.smileidentity.generated.models.EnrollParams;
AcceptedResponse accepted = smile.biometric().enroll(EnrollParams.builder()
.selfieImage(BinaryInput.of(new File("selfie.jpg")))
.livenessImages(livenessFrames)
.userDetails(user)
.consent(consent)
.userId("user-42")
.build());userId is required and must match an enrolled user. Images are required unless useEnrolledImage is true.
import com.smileidentity.generated.models.AuthenticationParams;
AcceptedResponse accepted = smile.biometric().authenticate(AuthenticationParams.builder()
.userId("user-42")
.selfieImage(BinaryInput.of(new File("selfie.jpg")))
.livenessImages(livenessFrames)
.userDetails(user)
.consent(consent)
.build());import com.smileidentity.generated.models.CompareParams;
import com.smileidentity.generated.models.ComparisonImageType;
AcceptedResponse accepted = smile.biometric().compare(CompareParams.builder()
.selfieImage(BinaryInput.of(new File("selfie.jpg")))
.comparisonImage(BinaryInput.of(new File("id_photo.jpg")))
.comparisonImageType(ComparisonImageType.ID_PHOTO)
.userDetails(user)
.consent(consent)
.build());import com.smileidentity.generated.models.JobStatus;
JobStatus status = smile.verifications().retrieve("job_01h2xcejqtf2nbrexx3vqjhp41");
status.isComplete(); // reached a decision
status.isProcessing(); // still running
status.isNotFound(); // a 404 returns this status instead of raising
status.getStatus(); // "processing", or the decision: "clear", "block", "attention", "error"While a job runs, status is processing. When it finishes, status carries the decision itself and message is a plain sentence such as Job completed. There is no literal complete status — read the decision from status, not from message. isComplete() is true for any status other than processing and not_found.
import com.smileidentity.helpers.WaitOptions;
import java.time.Duration;
JobStatus done = smile.verifications().waitUntilComplete(
accepted.getJobId(),
WaitOptions.builder()
.interval(Duration.ofSeconds(2))
.timeout(Duration.ofSeconds(120))
.treatNotFoundAsPending(true)
.build());
done.getStatus(); // "clear", "block", "attention" or "error"Polls while the job is processing and returns as soon as it reaches a decision. Raises com.smileidentity.errors.TimeoutException if the deadline passes.
import com.smileidentity.generated.models.ReplayCallbackResponse;
import com.smileidentity.generated.models.ReplayParams;
ReplayCallbackResponse replayed = smile.verifications().replay(
"job_01h2xcejqtf2nbrexx3vqjhp41",
ReplayParams.builder().callbackUrl("https://app.example.com/callback").build());A 409 means the verification is still processing; it raises ConflictException and is never retried automatically.
import com.smileidentity.generated.models.FraudReason;
import com.smileidentity.generated.models.ReportFraudParams;
smile.users().reportFraud("user-42", ReportFraudParams.builder()
.isFraud(true)
.reason(FraudReason.ACCOUNT_TAKEOVER)
.notes("Suspicious takeover pattern")
.reportedBy("fraud-team@example.com")
.build());
// Convenience wrappers:
smile.users().flagFraud("user-42", FraudReason.DOCUMENT_FORGERY, null, "fraud-team@example.com");
smile.users().clearFraud("user-42", "False positive after review", "fraud-team@example.com");import com.smileidentity.generated.models.SupportedDocumentsParams;
// No authentication needed for these three:
smile.services().bankCodes("NG");
smile.services().supportedIdTypes("NG");
smile.services().supportedDocuments(SupportedDocumentsParams.builder()
.continent("AFRICA").countryCode("NG").locale("en-GB").build());
// Token required:
smile.services().idStatus("NG", "NIN");All errors extend com.smileidentity.errors.SmileIDException and expose getStatusCode(), getStatus(), getMessage(), getCode(), getRequestId() and getRawBody().
| Class | When |
|---|---|
InvalidRequestException |
HTTP 400, 415 |
ValidationException |
Client-side validation, raised before sending |
AuthenticationException |
HTTP 401 (after one automatic token refresh) |
PaymentRequiredException |
HTTP 402: insufficient wallet balance |
PermissionException |
HTTP 403 |
NotFoundException |
HTTP 404 (never from verifications().retrieve) |
ConflictException |
HTTP 409: never auto-retried |
PayloadTooLargeException |
HTTP 413 |
RateLimitException |
HTTP 429 |
ApiException |
HTTP 5xx |
UnexpectedResponseException |
A 2xx response body that is not a JSON object |
ConnectionException |
Network failure or timeout, no HTTP response |
TimeoutException |
waitUntilComplete deadline passed |
import com.smileidentity.errors.PaymentRequiredException;
import com.smileidentity.errors.SmileIDException;
try {
smile.enhancedKyc().verify(params);
} catch (PaymentRequiredException e) {
// top up the wallet
} catch (SmileIDException e) {
System.err.println(e.getStatusCode() + ": " + e.getMessage());
}Retries: idempotent operations (status and services reads, plus the internal token fetch) are retried automatically on connection errors and HTTP 408, 429 and 5xx, with exponential backoff and support for Retry-After. Job-creating POSTs are never retried automatically — a connection failure surfaces as ConnectionException and the caller decides.
Every request carries SmileID-Source-SDK: java, SmileID-Source-SDK-Version and a User-Agent string identifying the SDK and Java runtime versions. These headers are observability metadata only; they are never used for authentication.
See AGENTS.md for how the codebase is laid out and how to run the test suite.
See SECURITY.md for how to report a vulnerability.
Licensed under the MIT License.