The Ripple Custody client SDKs replace the legacy Java SDK (Maven artifact com.metaco:sdk, loaded with HarmonizeContext.load(signer)). If your application uses the legacy SDK, use this page to move it to the new Java SDK. The page maps legacy concepts, configuration, and calls to the new SDK, and shows before-and-after code for common tasks.
The new SDK is also available for TypeScript and Python, with the same configuration model and intent lifecycle. If you're moving off the legacy SDK anyway, you can choose any of the three. For an overview of all three, see SDKs.
| Area | Legacy SDK | New Java SDK |
|---|---|---|
| Dependency | com.metaco:sdk, installed from a JAR with mvn install:install-file | com.ripple.custody.platform.contrib:ripple-custody-client-lib-java, also installed from a JAR and a Project Object Model (POM) file with mvn install:install-file. Requires Java 17 or later |
| Package root | com.metaco.harmonize | com.ripple.custody |
| Configuration | harmonize.json file or environment variables (GATEWAY_URL, OAUTH_URL, and others) | config.toml file or RC_* environment variables. Environment variables override the file |
| Entry point | HarmonizeContext.load(signer), then harmonizeCtx.harmonize | CustodyClient.fromConfig(CustodyClientConfig.loadConfiguration()) |
| Signing | You implement the Signer interface | Built-in signer for a PEM private key file, or your own ChallengeSigner and IntentSigner |
| Intent author | Derived from the signer | Set with RC_USER_ID and RC_DOMAIN_ID or config.author(...), or looked up through GET /v1/me |
| Resource access | Action classes per call, for example harmonize.users(domainId) | Fixed namespaces on one client, for example client.governance().users(), with the domain ID passed to each method |
| IDs | String | java.util.UUID |
| Entity creation and updates | Fluent builders ending in .submit(), returning IntentData | Namespace methods that build, sign, and submit the intent, returning PendingIntent |
| Waiting for results | waitForSucceed, waitForIntentStatus, and resource-specific waits, with a Timeout | waitAccepted, waitExecuted, and waitTxHash, with a timeout in seconds |
| Proposal expiry | Set per call with Expire | Fixed at one hour from submission |
| Errors | RuntimeException | CustodyError (unchecked), with the HTTP ApiException as its cause |
| Response signatures | Not verified | The SDK verifies them by default, which requires the notary API public key |
To install the new SDK, get the JAR and POM files from the Ripple support team. For full installation and setup steps, see Integrate via SDK.
Install the files into your local Maven repository, as you did for the legacy SDK. Replace {version} with the version you received:
mvn install:install-file \
-Dfile=ripple-custody-client-lib-java-{version}.jar \
-DpomFile=ripple-custody-client-lib-java-{version}.pomThen replace the legacy dependency with the new one:
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation 'com.ripple.custody.platform.contrib:ripple-custody-client-lib-java:{version}'
}The legacy SDK read harmonize.json from the home directory, the working directory, the --conf flag, or HARMONIZE_CONF, then applied environment variables. The new SDK reads the file named by RC_CONFIG_FILE, or ./config.toml if it exists, then applies RC_* environment variables, which take precedence.
The following table maps each legacy key to its replacement:
| Legacy key | New environment variable | New config.toml key | Notes |
|---|---|---|---|
HARMONIZE_CONF | RC_CONFIG_FILE | Not applicable | Path to the configuration file. The new SDK has no home-directory lookup and no --conf flag. |
GATEWAY_URL | RC_API_BASE_URL | api_base_url | Base URL of the Ripple Custody API. |
OAUTH_URL | RC_TOKEN_ENDPOINT | token_endpoint | OAuth 2.0 token endpoint. To have the SDK discover the token endpoint, set RC_OIDC_ISSUER_URL (oidc_issuer_url) instead. |
OAUTH_CLIENT_ID | RC_CLIENT_ID | client_id | Defaults to customer_api when you sign with a private key. |
Not applicable (the Signer implementation) | RC_PRIVATE_KEY_FILE | private_key_file | Path to the user's EC private key in PEM format. See Replace your signer. |
| Not applicable | RC_USER_ID | rc_user_id | ID of the user that authors intents. |
| Not applicable | RC_DOMAIN_ID | rc_domain_id | Default domain of the authoring user. |
| Not applicable | RC_NOTARY_API_PUBLIC_KEY | notary_api_public_key | The SDK requires this key while response verification is on. See Behavior differences. |
| Not applicable | RC_VERIFY_RESPONSES | verify_responses | true by default. |
TIMEOUT | None | None | Set timeouts on the underlying HTTP client, for example client.getApiClient().setReadTimeout(milliseconds) or setConnectTimeout(milliseconds). |
HTTP_VERSION | None | None | To control the HTTP client, pass your own OkHttpClient to ApiClient.newBuilder().httpClient(...). |
OPEN_API_VALIDATE_WARN_ONLY, OPEN_API_VALIDATE_STRICT_ENUMS | None | None | The new SDK doesn't check requests for conformance with the OpenAPI document at runtime. |
The new SDK also supports the client credentials grant (RC_CLIENT_ID with RC_CLIENT_SECRET), TLS settings, and address validation options. For the full list, see Configure the SDK.
The following example converts a legacy harmonize.json file:
{
"GATEWAY_URL": "https://custody.example.com/api",
"OAUTH_URL": "https://custody.example.com/oauth/token",
"OAUTH_CLIENT_ID": "customer_api"
}A single CustodyClientConfig and a long-lived CustodyClient replace the legacy HarmonizeContext, Context, and harmonize instance. CustodyClient.fromConfig resolves the signer, obtains OAuth tokens, and configures the HTTP client for you.
The following example creates a client with each SDK:
var harmonizeCtx = HarmonizeContext.load(signer);
// we get the Ripple Custody instance from the context
var harmonize = harmonizeCtx.harmonize;For a complete program, see example 00 (getting started). To get the examples, see Examples.
To load a specific file instead of RC_CONFIG_FILE or ./config.toml, call CustodyClientConfig.loadConfiguration(Path.of("/path/to/config.toml")). To read environment variables only, call CustodyClientConfig.fromEnvironment().
The legacy harmonizeCtx.switchSigner(newSigner) has no replacement in the new SDK. To act as a different user, create a second CustodyClientConfig and CustodyClient with that user's key and IDs.
The legacy SDK required you to implement the Signer interface and to serialize each message to canonical JSON before signing it. The new SDK canonicalizes every request for you and offers two signing options.
Private key file. If your user's private key is available as a PEM file, set RC_PRIVATE_KEY_FILE (or private_key_file). The SDK uses the key both to sign the OAuth login challenge and to sign intents. You don't write any signing code.
Your own key management system. If the key lives in a hardware security module (HSM) or a key management service, implement two signing callbacks and build the client yourself:
com.ripple.custody.auth.ChallengeSignersigns the OAuth login challenge and returns the user's public key.com.ripple.custody.IntentSignersigns the canonical JSON of each intent, approval, and rejection. It receives the canonical JSON string and returns a base64-encoded signature.
In the following example, myKeyService stands for your own integration with your key management system:
import com.ripple.custody.CustodyClient;
import com.ripple.custody.CustodyClientConfig;
import com.ripple.custody.auth.ChallengeSigner;
import com.ripple.custody.invoker.ApiClient;
CustodyClientConfig config = CustodyClientConfig.loadConfiguration();
ChallengeSigner challengeSigner = new ChallengeSigner(
challenge -> myKeyService.sign(challenge), // base64 signature
unused -> myKeyService.publicKeyBase64Der()); // base64 DER public key
ApiClient apiClient = ApiClient.newBuilder()
.tokenEndpoint(config.getEffectiveTokenEndpoint())
.clientId("customer_api")
.grantType("password")
.signer(challengeSigner)
.build();
apiClient.setBasePath(config.getApiBaseUrl());
config.apiClient(apiClient)
.signer(canonicalJson -> myKeyService.sign(canonicalJson));
CustodyClient client = new CustodyClient(config);The Java SDK doesn't include signers for cloud key management services or HashiCorp Vault. Setting RC_KMS_PROVIDER makes CustodyClient.fromConfig fail. Use custom ChallengeSigner and IntentSigner callbacks, as shown in the preceding example. The Java examples Example08HashiCorpVaultTransit, Example09AzureKeyVaultKeys, and Example10AwsKms show a signer for each provider. For details, see Authentication and signing.
Each legacy action class maps to a namespace on CustodyClient, as the following table shows. Namespace accessors are methods. Where the API requires a domain ID, every namespace method takes it as its first argument.
| Legacy SDK | New Java SDK |
|---|---|
HarmonizeContext.load(signer) | CustodyClient.fromConfig(CustodyClientConfig.loadConfiguration()) |
Config | CustodyClientConfig |
Signer | ChallengeSigner and IntentSigner, or RC_PRIVATE_KEY_FILE |
harmonize.domains() | client.governance().domains() |
harmonize.users(domainId) | client.governance().users() |
harmonize.policies(domainId) | client.governance().policies() |
harmonize.tickers() | client.governance().tickers() |
harmonize.intents(domainId) | client.governance().intents() |
harmonize.requests(domainId) | client.governance().requests() |
harmonize.events(domainId) | client.governance().events() |
harmonize.accounts(domainId) | client.accounts() |
harmonize.vaults() | client.vaults() |
harmonize.endpoints(domainId) | client.endpoints() |
harmonize.ledgers() | client.ledgers() |
harmonize.transactions(domainId) | client.transactions() |
harmonize.genesis() | client.genesis() |
harmonize.me() | No namespace method. Call new UsersApi(client.getApiClient()).getMe(). |
harmonize.notarize() | No namespace method. Use the generated IntentsApi directly. |
.get(id) returning Optional | get(domainId, id) throws CustodyError if the entity doesn't exist. find(domainId, id) returns Optional |
.list() returning a Stream | stream(domainId) pages through all results. list(domainId, limit, startingAfter) returns one page |
.create()...submit() and .create(entity, expire) | create(...) on the namespace, returning PendingIntent |
.update(...), .lock(...), .unlock(...) | update(...), lock(...), unlock(...) on the namespace, returning PendingIntent. Overloads accept a fetched entity |
IntentData | PendingIntent |
IntentData.getIntentId() | PendingIntent.getIntentId(), available after waitAccepted |
waitForSucceed(Timeout), waitForProcessingOrSucceeded(Timeout) | waitAccepted(timeoutSeconds) |
waitForIntentStatus(Set.of(IntentStatus.Executed), Timeout) | waitExecuted(timeoutSeconds) |
TransactionIntentData.waitForOrderTransactionProcessingStatus(...) | waitTxHash(timeoutSeconds) |
harmonize.intents(domainId).approve(intentId, expire) | pendingIntent.approve(comment), or client.governance().intents().approve(domainId, intentId, proposalSignature, comment) |
harmonize.intents(domainId).reject(intentId, reason, expire) | pendingIntent.reject(comment), or client.governance().intents().reject(domainId, intentId, proposalSignature, comment) |
MapJSON get(Class, key) and set(key, value) | Typed model classes in com.ripple.custody.models, each with getAdditionalProperties() and putAdditionalProperty(key, value) for fields the model doesn't declare |
The new SDK also has a client.webhooks() namespace for webhook channels, which the legacy SDK didn't offer.
In the legacy SDK, each action returned an IntentData and you chained a waitFor... method to poll the request and then the intent. In the new SDK, every method that proposes an intent signs and submits it, and returns a PendingIntent:
waitAccepted(timeoutSeconds)polls the request until Ripple Custody accepts or rejects it, and records the intent ID. It throwsCustodyErrorif the request fails. Call it first.waitExecuted(timeoutSeconds)polls the intent until it reaches a terminal status:Executed,Failed,Rejected, orExpired.waitTxHash(timeoutSeconds)supports transfer and transaction-order intents. It polls until the on-chain transaction reachesCompleted,Failed, orInterrupted.
If the timeout elapses first, the wait methods throw IntentTimeoutError. A timeout isn't a failure verdict: the intent might still execute later. If an intent needs approval from other users, don't block on waitExecuted. Check the intent later with client.governance().intents().get(domainId, intentId).
For more on the intent lifecycle, see Intents.
The examples in this section assume a configured client, as shown in Load the configuration and create a client.
The new create method requires at least one login ID and a public key in base64 DER format. It validates both before it sends the request. The following example creates a user with each SDK:
var domainId = "";
var userId = "";
var userAlias = "";
var userRoles = List.of("admin");
var userPublicKey = "";
var userDescription = "";
var user = Users.CreateUser.builder()
.id(userId)
.alias(userAlias)
.description(userDescription)
.roles(userRoles)
.publicKey(userPublicKey)
.build();
harmonize.users(domainId)
.create(user, Expire.hours(24))
.waitForSucceed(Timeout.minutes(1));For a complete program, see example 10 (create user). To get the examples, see Examples.
To create a domain, the new Java SDK has no create method on client.governance().domains(). Build a CoreV0CreateDomain payload and submit it with Envelope.submitIntent(client.getConfig(), payload, parentDomainId, null), which returns a PendingIntent.
The legacy TransactionActions builder maps to client.transactions().transfer(...) for transfers. When you need chain-specific transaction parameters, use client.transactions().createTransactionOrder(...) instead. A transfer names the ticker to send rather than a chain-specific parameter builder. The following example creates a transfer with each SDK:
var domainId = "";
var fromAccount = "";
var toAccount = "";
var amount = "5432";
var maxFee = "5432";
var transaction = Transactions.CreateTransactionOrder.builder()
.fromAccount(fromAccount)
.orderParameters(Transactions.btcParameters()
.maxFee(maxFee)
.priorityHigh()
.toAccount(domainId, toAccount)
.amount(amount)
.build()
)
.build();
harmonize.transactions(domainId).create(transaction, Expire.hours(24))
.waitForSucceed(Timeout.minutes(1));If you proposed the intent in the same process, approve it through the returned PendingIntent, which already holds the proposal signature. To approve an intent that someone else proposed, read the proposal signature from the intent and pass it to client.governance().intents().approve(...). To reject an intent, use reject the same way. The following example approves an intent with each SDK:
var domain = harmonize.domains().get(domainId).orElseThrow();
harmonize.intents(domain.getId())
.approve(intentData.getIntentId(),
Expire.hours(1))
.waitForSucceed(Timeout.minutes(1));
Intents.IntentEntity intent = harmonize.intents(domain.getId()).get(intentData.getIntentId()).orElseThrow();These differences change runtime behavior, not only names. Check each one before you switch over.
- The SDK verifies response signatures by default. The new SDK checks the notary signature on API responses and throws
ResponseSignatureErrorif verification fails. Creating a client fails unless you setRC_NOTARY_API_PUBLIC_KEY(orconfig.notaryApiPublicKey(...)), or explicitly opt out withRC_VERIFY_RESPONSES=false(orconfig.verifyResponses(false)). Get the notary API public key for your environment from your Ripple Custody administrator. For details, see Response verification. - The SDK handles canonical JSON for you. You no longer call
toJson()orGsonUtilsbefore signing. AnIntentSignerreceives the canonical JSON string and signs it as is. - No automatic retries. The SDK doesn't retry failed HTTP requests. Add your own retry logic where your application needs it, and make sure retried intent submissions are safe to repeat. For details, see Errors and HTTP.
- Every intent must have an author. Set
RC_USER_IDandRC_DOMAIN_ID, or callconfig.author(...)with aCoreUserReference. Otherwise, the SDK looks up your user throughGET /v1/me. If you belong to several domains, setRC_DOMAIN_ID. - Waiting for execution doesn't throw on a failed intent.
waitExecutedreturns the intent when it reaches any terminal status:Executed,Failed,Rejected, orExpired. Checkpending.getStatus()or the returned intent's state. The legacywaitFormethods threw aRuntimeExceptionfor failure states. - Proposals have a fixed expiry. Intents that you propose through the new SDK expire one hour after submission. The new SDK has no
Expireargument. - Updates replace list fields.
updatemethods on users, policies, endpoints, and tickers replace list fields such as roles, login IDs, and workflow conditions in full. Read the current entity and pass its existing values back unless you intend to remove them. - Address validation is on by default. Before it sends the request,
transferrejects a destination address that isn't in a recognized format for any supported chain. To turn this off, setRC_VALIDATE_ADDRESSES=false.
- Integrate via SDK: install the SDK, connect, and submit a first intent.
- Configure the SDK: all configuration keys and environment variables.
- Authentication and signing: grants, private keys, and custom signers.
- Intents: propose, approve, reject, and track intents.
- Errors and HTTP: error types, timeouts, and custom HTTP clients.