Retorna Gateway SDK for Python
Official Python SDK for Retorna Gateway (the Retorna B2B API): quotations, orders, wallets and delivery routes. Successor of retorna-python-sdk 1.x; an idiomatic port of the Retorna Gateway SDK for Java, speaking the same contract. The import name is retorna_sdk.
Built in: OAuth2 client_credentials authentication, automatic token refresh, RSA request signing, retries with exponential backoff, explicit idempotency keys, and full type hints.
One runtime dependency — cryptography, because RSA signing is not in the standard library. HTTP is urllib, models are dataclasses, JSON is json. Nothing else lands in your dependency tree.
Highlights vs v1
- Authentication is two independent proofs: an OAuth2 bearer token AND an RSA signature on every request. A valid token on its own is a
401. /me/*endpoints are token-scoped — one credential pair = one company. No moreowner_idparameter.- Monetary amounts are decimal strings (
"56.00","100.000001"), neverfloat. The SDK keeps them asstrend to end. - Wallet transactions moved from offset to cursor pagination. Sending
pageis now a400.
Requirements
- Python 3.9 or higher.
- Retorna-issued
client_id,client_secret, and an RSA private key in PKCS#8 PEM form.
Installation
pip install retorna-gateway-sdk
uv add retorna-gateway-sdk
poetry add retorna-gateway-sdk
Quickstart
The full example lives at examples/quickstart/quickstart.py.
import os
from retorna_sdk import (
RetornaClient, CreateQuotationRequest, QuotationSource, QuotationDestination,
QuotationPayoutMethod, QuotationQuote, PayoutMethodType, QuoteMode,
)
client = RetornaClient.create(
environment="DEVELOP",
client_id=os.environ["RETORNA_CLIENT_ID"],
client_secret=os.environ["RETORNA_CLIENT_SECRET"],
private_key=os.environ["RETORNA_PRIVATE_KEY"], # signs every request
)
# 1. Balance (token-scoped — no company id anywhere)
wallet = client.get_my_wallet("USDR")
print(wallet.amount, wallet.currency)
# 2. Available corridors
routes = client.get_routes("USDR")
# 3. Lock a rate
quote = client.create_quotation(CreateQuotationRequest(
source=QuotationSource("USDR"),
destination=QuotationDestination(
country="VE", currency="VES",
payout_method=QuotationPayoutMethod(PayoutMethodType.BANK_TRANSFER),
),
quote=QuotationQuote(QuoteMode.SEND_EXACT, "10.00"),
))
print(quote.exchange_rate.value, quote.target.amount)
Two ways to build a client
RetornaClient.create(...) is the Python front door. The fluent builder mirrors the Java SDK and is there when you need to inspect or reuse a config:
from retorna_sdk import RetornaClientBuilder, RetornaEnvironment
client = (
RetornaClientBuilder()
.environment(RetornaEnvironment.DEVELOP)
.client_id(...).client_secret(...).private_key(...)
.retries(3).backoff_ms(200)
.logging_level("DEBUG")
.build_client()
)
Three things that will bite you
1. The private key is not optional
b2b-service runs a global signature guard. Every api-channel request carries signature and nonce headers, checked against your tenant's public key independently of the bearer token. A perfectly valid token with no signature gets a 401.
The key must be PKCS#8 (-----BEGIN PRIVATE KEY-----). If yours starts with BEGIN RSA PRIVATE KEY it is PKCS#1:
openssl pkcs8 -topk8 -nocrypt -in key.pem -out key-pkcs8.pem
The SDK accepts the PEM with real newlines, with \n escapes, or flattened onto one line with spaces — which is the shape AWS Secrets Manager hands back.
2. Money is a string
wallet.amount # "1234.567890" <- str, always
float(wallet.amount) # DON'T. IEEE-754 cannot represent 0.1.
When you need arithmetic, go through Decimal:
from retorna_sdk.core import money
total = money.parse(wallet.amount) + money.parse("10.50")
amount_for_the_wire = money.format(total) # back to str
3. Every 401 looks the same
Bad credentials, an expired token, and a bad request signature all surface as a bare 401 B2B_UNAUTHORIZED. The guard never says which check failed. When you are debugging one, that ambiguity is the problem — start by confirming the key matches the client_id.
The SDK will not retry a 401 when it holds a token it believes is still fresh, precisely because that case means credentials or signature, not staleness.
Error handling
Three exception types, all subclasses of RetornaError:
from retorna_sdk import RetornaB2BError, RetornaAuthError, RetornaSdkError
try:
order = client.create_order(request, idempotency_key="invoice-42")
except RetornaB2BError as e:
# The API understood the request and refused it.
e.code # "B2B_INSUFFICIENT_BALANCE" (raw wire string, always preserved)
e.code_enum # B2BErrorCode.B2B_INSUFFICIENT_BALANCE
e.category # B2BErrorCategory.PERMANENT
e.http_status # 422
e.correlation_id # what support will ask you for
e.is_retryable # only TRANSIENT errors are
except RetornaAuthError as e:
# Token endpoint rejection, or the API rejecting the token/signature.
e.http_status
except RetornaSdkError as e:
# Transport, JSON, or an unstructured 5xx.
e.context # "OrdersClient.create_order"
e.cause
An error code the SDK does not know about still arrives as a RetornaB2BError with code intact and code_enum == B2B_UNKNOWN, so a new backend code never breaks a deployed integration.
Bad arguments raise plain ValueError instead, because a malformed UUID is your bug, caught before anything reaches the network.
Idempotency
POST /orders requires an X-Idempotency-Key. It is free-form — the backend only checks that it is present, not that it is a UUID. It round-trips as the order's external_id and is a list filter, so your own invoice number is the intended value:
order = client.create_order(request, idempotency_key="INV-2026-0042")
...
found = client.list_orders(ListOrdersParams.by_external_id("INV-2026-0042"))
Reuse the same key when retrying and the server deduplicates instead of paying twice. Pass nothing and the SDK generates a UUID — safe, but you lose the ability to correlate a retry.
Pagination
Cursor-based, for both orders and wallet transactions:
from retorna_sdk import ListOrdersParams
cursor = None
while True:
page = client.list_orders(ListOrdersParams(cursor=cursor, limit=100))
for order in page.data or []:
...
if not page.pagination.has_more:
break
cursor = page.pagination.next_cursor
pagination.total is the count matching your filters, not the page size, and it may be None.
API surface
| Operation | Method | Endpoint |
|---|---|---|
| Create quotation | create_quotation(request) |
POST /quotations |
| Get quotation | get_quotation(id) |
GET /quotations/{id} |
| Create order | create_order(request, idempotency_key) |
POST /orders |
| Get order | get_order(id) |
GET /orders/{id} |
| List orders | list_orders(params) |
GET /orders |
| Get routes | get_routes(source_currency) |
GET /me/routes |
| Get company | get_my_client() |
GET /me/client |
| Get wallet | get_my_wallet(currency) |
GET /me/wallets/{currency} |
| Wallet transactions | get_my_wallet_transactions(currency, type, params) |
GET /me/wallets/{currency}/transactions/{type} |
Each is also reachable through its sub-client: client.orders.create_order(...), client.wallets.get_my_wallet(...), and so on.
Resource ids are opaque strings of 1..100 characters. Do not assume they are UUIDs — dev returns UUID-shaped ids today, the orders spec documents cpg_txn_a1b2c3d4e5f6, and both are within contract.
Configuration
| Option | Default | Notes |
|---|---|---|
environment |
PRODUCTION |
DEVELOP, SANDBOX, PRODUCTION |
retries |
3 |
Retried on 429 and 5xx only |
backoff_ms |
200 |
Exponential: backoff_ms * 2**attempt |
connect_timeout |
10.0 |
Seconds |
request_timeout |
10.0 |
Seconds |
logging_level |
ERROR |
NONE, ERROR, WARN, INFO, DEBUG — writes to stderr |
ssl_context |
None |
For mutual TLS; see retorna_sdk.core.tls |
base_url_override / auth_url_override |
None |
https only |
Environments resolve to:
| Environment | Base URL | Scope |
|---|---|---|
DEVELOP |
https://api.gateway.dev.retorna.app |
sandbox/full_access |
SANDBOX |
https://api.gateway.sandbox.retorna.app |
sandbox/full_access |
PRODUCTION |
https://api.gateway.retorna.app |
prod/full_access |
SANDBOX is the environment the public API documentation calls Sandbox; infra names the same stack stg, which is why its Cognito hosted domain says b2b-retorna-stg.
Identity headers
Every request carries two headers that tell the platform which SDK is calling, so adoption per version can be tracked and support can tell a Python integration from a Java one:
X-Retorna-Client: python-sdk/1.0.0
User-Agent: python-sdk/1.0.0 (python/3.12.4; Linux x86_64)
The python-sdk token is fixed and independent of the PyPI distribution name. Nothing sensitive is sent.
Development
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
make test # unit tests
make lint # ruff
make typecheck # mypy --strict
make check # all three
make integration # live DEVELOP tests (needs credentials)
The integration suite is read-only by design — it never calls create_order, which moves real funds. Keep it that way.
Relationship to the other SDKs
This SDK is a port of the Retorna Gateway SDK for Java (retorna-java-sdk-v2), the reference implementation. The request-signing logic is verified byte-for-byte against it. All three SDKs (retorna-gateway-sdk on npm, Maven Central and PyPI) share the same contract, the same environment names and the same X-Retorna-Client identity header.
License
Proprietary. Use is limited to integrating your systems with Retorna Gateway under a commercial agreement with Retorna — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file retorna_gateway_sdk-1.0.0.tar.gz.
File metadata
- Download URL: retorna_gateway_sdk-1.0.0.tar.gz
- Upload date:
- Size: 70.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4ecb0f325a6fabb54d57562e3f90ecc0fa1de5fa9ef9019363bfca32b9cd9a25
|
|
| MD5 |
cb5fe65a89003669b287f7940bc055c9
|
|
| BLAKE2b-256 |
bb1c10cf459ec0247fcbcb49f01ff8eb4f30219ec34b5a5983db41765c1f1822
|
File details
Details for the file retorna_gateway_sdk-1.0.0-py3-none-any.whl.
File metadata
- Download URL: retorna_gateway_sdk-1.0.0-py3-none-any.whl
- Upload date:
- Size: 61.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
22e2ba9ac0e001772f8749fe6cdc6d62c49d5c69c625a4c48b6edc556d95ef48
|
|
| MD5 |
58ade63379f573d9069ec46a67896928
|
|
| BLAKE2b-256 |
9f3324117110022cb0d47b82af5a5c6af25c0f1e083a904a2610645a7a2bb0b5
|