Getting Started
Mobile & Embedded Quickstart
License a mobile app or embedded device by calling the REST API directly. No SDK required.
What you need
- An HTTPS client (URLSession on iOS, OkHttp on Android, libcurl on embedded).
- A JSON parser (the platform default is fine — JSONDecoder, org.json, jq).
- A SHA-256 implementation (in standard library on every modern platform).
- A stable per-device identifier — see the fingerprint sections below.
iOS / macOS (Swift)
Validate a license key with a machine fingerprint. The endpoint returns 200 with { valid, code, ... } on success and a 4xx with a typed code on error.
import Foundation
struct ValidationResult: Decodable {
let valid: Bool
let code: String
let machine: Machine?
struct Machine: Decodable {
let id: String
let fingerprint: String
}
}
struct LicentricError: Error { let code: String; let message: String }
func validateLicense(key: String, fingerprint: String) async throws -> ValidationResult {
var request = URLRequest(url: URL(string: "https://licentric.com/api/v1/licenses/validate-key")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Licentric-iOS/1.0", forHTTPHeaderField: "User-Agent")
let body: [String: Any] = ["key": key, "fingerprint": fingerprint]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw LicentricError(code: "NETWORK_ERROR", message: "No HTTP response")
}
if (200...299).contains(http.statusCode) {
let envelope = try JSONDecoder().decode([String: ValidationResult].self, from: data)
return envelope["data"]!
}
// Map license-state codes to typed errors so callers can branch on type
if let body = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let code = body["code"] as? String {
throw LicentricError(code: code, message: body["error"] as? String ?? "Unknown")
}
throw LicentricError(code: "UNKNOWN", message: "HTTP \(http.statusCode)")
}iOS device fingerprint
import UIKit
import CryptoKit
/// Stable iOS device identifier — survives app reinstall as long as the
/// vendor-bundle identifier stays the same. For per-keychain stability
/// across app updates, store the result in the iOS Keychain on first use.
func deviceFingerprint() -> String {
let raw = UIDevice.current.identifierForVendor?.uuidString ?? UIDevice.current.name
let hash = SHA256.hash(data: Data(raw.utf8))
return hash.compactMap { String(format: "%02x", $0) }.joined()
}Android (Kotlin)
Same flow, OkHttp client. Catch LicentricError and branch on errorCode — values like LICENSE_REVOKED, LICENSE_SUSPENDED, and LICENSE_EXPIRED are documented in the errors reference.
import okhttp3.*
import org.json.JSONObject
import java.io.IOException
data class ValidationResult(val valid: Boolean, val code: String)
class LicentricError(val errorCode: String, message: String) : Exception(message)
class LicentricClient(private val baseUrl: String = "https://licentric.com") {
private val client = OkHttpClient()
@Throws(LicentricError::class)
fun validate(licenseKey: String, fingerprint: String): ValidationResult {
val body = JSONObject()
.put("key", licenseKey)
.put("fingerprint", fingerprint)
.toString()
.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("$baseUrl/api/v1/licenses/validate-key")
.post(body)
.header("User-Agent", "Licentric-Android/1.0")
.build()
client.newCall(request).execute().use { response ->
val responseBody = response.body?.string() ?: throw LicentricError("EMPTY", "no body")
if (response.isSuccessful) {
val data = JSONObject(responseBody).getJSONObject("data")
return ValidationResult(data.getBoolean("valid"), data.getString("code"))
}
// Map license-state codes (e.g. "LICENSE_REVOKED") to typed errors
val errorJson = JSONObject(responseBody)
throw LicentricError(
errorJson.optString("code", "UNKNOWN"),
errorJson.optString("error", "Validation failed"),
)
}
}
}Android device fingerprint
import android.content.Context
import android.provider.Settings
import java.security.MessageDigest
/// Stable Android device identifier — Settings.Secure.ANDROID_ID is unique
/// per app-signing-key + per-user since Android 8.0. For pre-O fallback,
/// see Google Play Services Instance ID.
fun deviceFingerprint(context: Context): String {
val androidId = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID,
) ?: "unknown"
val digest = MessageDigest.getInstance("SHA-256").digest(androidId.toByteArray())
return digest.joinToString("") { "%02x".format(it) }
}Embedded Linux (shell)
For headless devices that already have curl + jq, validation is a few lines. /etc/machine-id provides a stable per-host fingerprint.
# Headless Linux device example — busybox + curl + jq
LICENSE_KEY="LIC-XXXX-XXXX-XXXX-XXXX"
FP=$(cat /etc/machine-id | sha256sum | awk '{print $1}')
response=$(curl -s -w "\n%{http_code}" \
-X POST https://licentric.com/api/v1/licenses/validate-key \
-H "Content-Type: application/json" \
-d "{\"key\":\"$LICENSE_KEY\",\"fingerprint\":\"$FP\"}")
http_code=$(echo "$response" | tail -1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" = "200" ]; then
valid=$(echo "$body" | jq -r '.data.valid')
code=$(echo "$body" | jq -r '.data.code')
echo "License is $code (valid=$valid)"
else
echo "Validation failed: HTTP $http_code"
echo "$body" | jq -r '.code + ": " + .error'
fiError handling
Treat license-state error codes as fail-immediately — surface the message to the end user without retrying. Treat network errors and 5xx responses as retry-eligible — fall back to the cached result if available.
LICENSE_REVOKED— show “License revoked. Contact support.” Do not retry.LICENSE_SUSPENDED— show “License on hold.” Polling later may recover.LICENSE_EXPIRED— show renewal flow.RATE_LIMITED— back off using theRetry-Afterheader.UNAUTHORIZED,VALIDATION_ERROR— bug in the request; fix client code.
POST /api/v1/licenses/{id}/checkoutfrom your backend, deliver the signed file to the device, and verify it locally with the platform's Ed25519 implementation. iOS: CryptoKit. Android: BouncyCastle. Embedded: libsodium. The signature format is documented in Offline Licensing.