Skip to main content

Python SDK

A typed, fluent client for the Retorna B2B API: build a quotation, turn it into an order, check your wallet, and list the routes your company is enabled for β€” without building HTTP requests or signing anything by hand. Currently pinned to 1.0.4, and this page tracks that version.

Install​

pip install retorna-gateway-sdk
# or
uv add retorna-gateway-sdk
# or
poetry add retorna-gateway-sdk

Requires Python 3.9+. The only runtime dependency is cryptography, used for RSA key parsing and request signing β€” everything else is the standard library. The package is retorna-gateway-sdk; the module you import is retorna_sdk.

Configure the client​

Every example below runs against sandbox. Build a client once and reuse it β€” it holds your credentials and a connection pool, not a single request.

import os
from retorna_sdk import RetornaClient

client = RetornaClient.create(
environment="SANDBOX",
client_id=os.environ["RETORNA_CLIENT_ID"],
client_secret=os.environ["RETORNA_CLIENT_SECRET"],
private_key=os.environ["RETORNA_PRIVATE_KEY"], # PKCS#8 PEM
)

There is also a builder, if you prefer assembling the config step by step β€” RetornaClientBuilder().environment("SANDBOX")...build_client() gets you the same client. Watch the name: build_client() returns a ready RetornaClient; the shorter build() only returns the resolved configuration object, not a client you can call.

create() accepts these keyword arguments:

OptionDefaultNotes
environmentPRODUCTIONSet it explicitly β€” the default is not sandbox
scopeenvironment defaultOAuth2 scope, rarely overridden
logging_levelERRORSDK-internal logging verbosity
retries3Automatic retries on transient failures
backoff_ms200Base delay between retries
connect_timeout10.0Seconds
request_timeout10.0Seconds
ssl_contextnoneCustom ssl.SSLContext, for advanced TLS setups
base_url_overridenonePoint at a non-standard host, e.g. for local testing

Environments​

EnvironmentBase URL
DEVELOPhttps://api.gateway.dev.retorna.app
SANDBOXhttps://api.gateway.sandbox.retorna.app
PRODUCTIONhttps://api.gateway.retorna.app

Your first call​

Confirm the client is wired up correctly before doing anything else:

me = client.get_my_client()
print(me.client.name, me.client.is_active, me.client.permissions)

Quotation β†’ order​

A quotation locks an exchange rate for a short window; reference its id when creating the order. Full method docs β€” every field, return value and error code β€” live on the Quotations and Orders pages; here is the shape of the whole flow in one example:

import os, uuid
from retorna_sdk import (
CreateQuotationRequest, QuotationSource, QuotationDestination,
QuotationPayoutMethod, QuotationQuote, QuoteMode, PayoutMethodType,
CreateOrderRequest, OrderSender, OrderReceiver, PersonDocument,
OrderDestination, PaymentInstructions, BankPhoneAccount,
FinancialEntity, OrderPurpose,
)

quotation = client.create_quotation(CreateQuotationRequest(
source=QuotationSource(currency="USDR"),
destination=QuotationDestination(
country="VE", currency="VES",
payout_method=QuotationPayoutMethod(type=PayoutMethodType.P2P_PHONE_TRANSFER)),
quote=QuotationQuote(mode=QuoteMode.SEND_EXACT, amount="56.00"),
))

request = CreateOrderRequest.with_purpose(
quotation_id=quotation.id,
sender=OrderSender(names="Ana MarΓ­a", last_names="PΓ©rez GΓ³mez",
document=PersonDocument(type="CC", id="1020304050", country="CO"),
country="CO"),
destination=OrderDestination(
receiver=OrderReceiver(names="Carlos AndrΓ©s", last_names="RodrΓ­guez LΓ³pez",
document=PersonDocument(type="V", id="12345678", country="VE"),
email="[email protected]", country="VE"),
payment_instructions=PaymentInstructions.of_bank_phone_account(
BankPhoneAccount.of(FinancialEntity.BNC_VE, "+584121234567", "12345678"))),
purpose=OrderPurpose.FAMILY_SUPPORT,
)

idempotency_key = f"invoice-{uuid.uuid4()}" # free-form str, NOT a uuid.UUID
if os.environ.get("RETORNA_CREATE_ORDER") == "yes":
order = client.create_order(request, idempotency_key)
print(order.id, order.status, order.idempotency_key)

QuoteMode.SEND_EXACT means the sender parts with exactly 56.00; RECEIVE_EXACT describes what the recipient gets instead. idempotency_key is a plain str, not a UUID type, and it comes back as idempotency_key on the order β€” reusing it with a different request body returns B2B_IDEMPOTENCY_CONFLICT (409) instead of the original order. The RETORNA_CREATE_ORDER gate above is only there so this snippet is safe to paste and run without moving real money by accident β€” drop it in your own code once you are ready.

Objects​

Quotations β€” lock an exchange rate, fees and amounts before creating an order. See Quotations for create_quotation and get_quotation.

Orders β€” the actual movement of money, created from a quotation. See Orders for create_order, list_orders and get_order.

Routes β€” the corridors (destination country, currency, payout method) your company is enabled for. See Routes for get_routes.

Wallets β€” your company's balance and top-up history. See Wallets for get_my_wallet and get_my_wallet_transactions.

Client β€” who the credentials belong to. See Client for get_my_client.

Errors​

Every error the SDK raises is one of three classes, all ending in Error β€” not Exception β€” and all importable from the top-level package. RetornaAuthError, RetornaB2BError, and RetornaSdkError all extend RetornaError, so a single except RetornaError catches everything the SDK raises:

from retorna_sdk import RetornaAuthError, RetornaB2BError, RetornaSdkError

try:
order = client.create_order(request, idempotency_key)
except RetornaB2BError as e:
if e.code == "B2B_IDEMPOTENCY_CONFLICT":
... # same key, different payload
elif e.is_retryable:
... # a TRANSIENT category error
print(e.code, e.code_enum, e.http_status, e.correlation_id)
except RetornaAuthError as e:
... # token OR signature β€” the API never says which
except RetornaSdkError as e:
print(e.context, e.cause) # transport / JSON / unstructured 5xx

RetornaB2BError carries the wire code verbatim as a string, plus code_enum for exhaustive matching and is_retryable for a quick branch. For the full list of codes and what each one means, see the error catalog and the error handling guide β€” this page deliberately does not duplicate that table.

Things that will bite you​

The key must be PKCS#8

private_key has to be a PKCS#8 PEM (-----BEGIN PRIVATE KEY-----). A PKCS#1 key (-----BEGIN RSA PRIVATE KEY-----) fails immediately with an actionable error at client construction β€” convert it first:

openssl pkcs8 -topk8 -nocrypt -in key.pem -out key-pkcs8.pem
Money is a string

Amounts are always str on the wire β€” "56.00", never 56.0. Do not put them in a float: IEEE-754 cannot represent 0.1 exactly, and a rounding error in a payment amount is a real bug. Use retorna_sdk.core.money.parse / .format when you need actual arithmetic, and convert back to str before sending.

Every 401 looks the same

A bad bearer token and a bad request signature both come back as the same RetornaAuthError with a bare 401. The SDK β€” and the API β€” cannot tell you which one failed. If you hit this, check both independently rather than assuming it is always the token.

Next steps​