Skip to main content

Java SDK

A typed client for the Retorna B2B API. It wraps the raw HTTP calls from the quickstart so you stop hand-rolling the OAuth2 token exchange and the per-request signature: the SDK acquires and caches the token for you and signs every outgoing request under the hood. If you want to understand what it is doing on your behalf, read authentication and request signing — the SDK follows those rules exactly, it just saves you from implementing them.

Requirements​

  • Java 17 or newer.
  • Jackson is the only runtime dependency (jackson-databind + jackson-datatype-jsr310), pulled in transitively — you do not declare it yourself.
  • No bundled HTTP library. Requests go out over the JDK's built-in java.net.http.HttpClient, so there is nothing else to pull in.

Install​

Maven

<dependency>
<groupId>app.retorna</groupId>
<artifactId>retorna-gateway-sdk</artifactId>
<version>1.0.3</version>
</dependency>

Gradle (Kotlin DSL)

implementation("app.retorna:retorna-gateway-sdk:1.0.3")

Gradle (Groovy DSL)

implementation 'app.retorna:retorna-gateway-sdk:1.0.3'

Configure the client​

import app.retorna.sdk.RetornaClient;
import app.retorna.sdk.RetornaClientBuilder;
import app.retorna.sdk.core.environment.RetornaEnvironment;

RetornaClient client = new RetornaClientBuilder()
.environment(RetornaEnvironment.SANDBOX)
.clientId(System.getenv("RETORNA_CLIENT_ID"))
.clientSecret(System.getenv("RETORNA_CLIENT_SECRET"))
.privateKey(System.getenv("RETORNA_PRIVATE_KEY")) // PKCS#8 PEM
.retries(3)
.backoffMs(200L)
.buildClient();
The private key is not optional

clientId/clientSecret buy you a token; the private key signs every request. These are two independent checks, so a client built without a key still gets back a 401 — one that reads exactly like bad credentials, even though the token was fine. Set all three.

The key must be a PKCS#8 PEM (-----BEGIN PRIVATE KEY-----). Real newlines, \n-escaped newlines, and the flattened single-line form all parse — whatever shape your secret manager hands you works. It is decoded once, at build time, so a malformed key fails immediately instead of on your first request.

build() is not buildClient()

RetornaClientBuilder has two terminal methods. build() only validates your configuration and returns an SdkConfig — it does not give you a working client. buildClient() does both: validates and returns the RetornaClient you actually call. Reach for buildClient().

Environments​

ConstantBase URL
RetornaEnvironment.SANDBOXhttps://api.gateway.sandbox.retorna.app
RetornaEnvironment.PRODUCTIONhttps://api.gateway.retorna.app
RetornaEnvironment.DEVELOPInternal use only — no public base URL.

Picking an environment is the only configuration step: the SDK resolves the right base URL for you, so nothing else in your code changes between sandbox and production besides the enum value and your credentials.

Sub-clients​

Every method shown below is also reachable through a domain-scoped sub-client, and both paths call the same code:

client.createQuotation(request); // flat method
client.quotations().createQuotation(request); // same call, through the sub-client

RetornaClient exposes one accessor per domain object: quotations(), orders(), wallets(), routes(), clients(). Reach for the sub-client when you are passing the domain client around as its own dependency (a QuotationsClient field, for example) instead of the whole RetornaClient; otherwise the flat methods read just as well.

First call​

A quick way to confirm your credentials and signature are both correct before touching money movement:

ClientResponse me = client.getMyClient();
System.out.printf("%s (active=%s)%n", me.client().name(), me.client().isActive());

If this throws, jump to Handling errors below — the exception tells you whether it was your token or your signature.

Quotation → order​

Every payout starts with a quotation, then an order that references it:

QuotationDetail quote = client.createQuotation(new CreateQuotationRequest(
new QuotationSource("USDR"),
new QuotationDestination("VE", "VES",
new QuotationPayoutMethod(PayoutMethodType.P2P_PHONE_TRANSFER)),
new QuotationQuote(QuoteMode.SEND_EXACT, "10.00")));

System.out.printf("rate=%s, receiver gets %s %s%n",
quote.exchangeRate().value(), quote.target().amount(), quote.target().currency());

String idempotencyKey = "invoice-2026-0042"; // your own reference

Order order = client.createOrder(
CreateOrderRequest.withPurpose(
quote.id(),
null, // metadata, echoed back untouched
OrderPurpose.FAMILY_SUPPORT, // required, closed catalog
new OrderSender("Ana María", "Pérez Gómez",
new PersonDocument("CC", "1020304050", "CO"), "CO"),
new OrderDestination(
new OrderReceiver("Carlos Andrés", "Rodríguez López",
new PersonDocument("V", "12345678", "VE"),
PaymentInstructions.ofBankPhoneAccount(
BankPhoneAccount.of(FinancialEntity.BNC_VE,
"+584121234567", "12345678")))),
idempotencyKey);

System.out.printf("Order %s status=%s idempotencyKey=%s%n",
order.id(), order.status(), order.idempotencyKey());

A quotation is single-use: it backs exactly one order, forever. Pass a real idempotency key on createOrder — an invoice number beats a random UUID, because it round-trips as order.idempotencyKey() and lets you find this order later. Reuse the same key on a retry; the server deduplicates for you. There is no idempotencyKey field on the request itself — only on the response — so do not try to set one.

The bank account's bankName comes from a closed catalog: FinancialEntity constants like BANCO_BANESCO_VE, never the bank's commercial name. See the full list under bank entities.

Objects​

Each domain object has its own page with every method, its parameters, its return shape, and the errors it can raise:

  • Quotations — price a corridor and payout method before committing to a transfer: createQuotation, getQuotation.
  • Orders — create, read, and list the payouts themselves: createOrder, getOrder, listOrders.
  • Routes — the corridors your company is configured to send through, with their rates, fees, and limits: getRoutes.
  • Wallets — your company's balance and top-up history: getMyWallet, getMyWalletTransactions.
  • Client — identity and permissions of the authenticated client: getMyClient.

Handling errors​

The SDK throws one of three exception types, all subclasses of RetornaException:

try {
Order order = client.createOrder(request, idempotencyKey);
} catch (RetornaB2BException e) {
if (e.codeEnum().isRetryable()) {
// TRANSIENT — retry with the SAME idempotency key
} else {
System.err.printf("%s [%s, http=%d] %s (correlationId=%s)%n",
e.code(), e.category(), e.httpStatus(), e.getMessage(), e.correlationId());
}
} catch (RetornaAuthException e) {
System.err.printf("Auth failed (HTTP %d): %s%n", e.httpStatus(), e.getMessage());
} catch (RetornaSdkException e) {
System.err.printf("SDK error in %s: %s%n", e.context(), e.getMessage());
}

Branch on code() — the raw wire string — rather than getMessage(), which is written for humans and can change. codeEnum() degrades gracefully to an unknown, non-retryable value when the backend adds a code this SDK version does not know about yet, so your branching never breaks on an upgrade.

This page does not repeat the error catalog. See error codes for the full list of codes, statuses and categories, and handling errors for how the different failure shapes relate to each other.

One client, many threads​

Build a single RetornaClient when your application starts and reuse it for every request — do not construct one per call. Under the hood it keeps exactly one java.net.http.HttpClient instance, built once at construction time and reused for every outgoing call. It is safe to share: the token cache refreshes itself safely under concurrent access, and signing a request touches no shared mutable state. RetornaClient implements AutoCloseable, so try-with-resources works, though closing it today has nothing to release.

Every outgoing request also carries an X-Retorna-Client header identifying the SDK and its version — useful context if you ever need to reference a request in a support conversation.

Where to next​