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:
| Option | Default | Notes |
|---|---|---|
environment | PRODUCTION | Set it explicitly β the default is not sandbox |
scope | environment default | OAuth2 scope, rarely overridden |
logging_level | ERROR | SDK-internal logging verbosity |
retries | 3 | Automatic retries on transient failures |
backoff_ms | 200 | Base delay between retries |
connect_timeout | 10.0 | Seconds |
request_timeout | 10.0 | Seconds |
ssl_context | none | Custom ssl.SSLContext, for advanced TLS setups |
base_url_override | none | Point at a non-standard host, e.g. for local testing |
Environmentsβ
| Environment | Base URL |
|---|---|
DEVELOP | https://api.gateway.dev.retorna.app |
SANDBOX | https://api.gateway.sandbox.retorna.app |
PRODUCTION | https://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"),
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β
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
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.
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β
- Authentication β how the token and signature fit together
- Request signing β the exact bytes that get signed
- Idempotency β safe retries for
create_order - Pagination β cursors across orders and wallet transactions
- Handling errors β the four response shapes you can receive
- Error catalog β every
codeand whether it is retryable - Payout types β bank transfer vs. phone transfer
- USDR β the currency used throughout this page
- Bank entities β Venezuela β the full
FinancialEntitycatalog - Orders API reference