Skip to main content

Retorna Gateway SDK for Python

PyPI Python License: Proprietary

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 dependencycryptography, 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 more owner_id parameter.
  • Monetary amounts are decimal strings ("56.00", "100.000001"), never float. The SDK keeps them as str end to end.
  • Wallet transactions moved from offset to cursor pagination. Sending page is now a 400.

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

retorna_gateway_sdk-1.0.0.tar.gz (70.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

retorna_gateway_sdk-1.0.0-py3-none-any.whl (61.9 kB view details)

Uploaded Python 3

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

Hashes for retorna_gateway_sdk-1.0.0.tar.gz
Algorithm Hash digest
SHA256 4ecb0f325a6fabb54d57562e3f90ecc0fa1de5fa9ef9019363bfca32b9cd9a25
MD5 cb5fe65a89003669b287f7940bc055c9
BLAKE2b-256 bb1c10cf459ec0247fcbcb49f01ff8eb4f30219ec34b5a5983db41765c1f1822

See more details on using hashes here.

File details

Details for the file retorna_gateway_sdk-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for retorna_gateway_sdk-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 22e2ba9ac0e001772f8749fe6cdc6d62c49d5c69c625a4c48b6edc556d95ef48
MD5 58ade63379f573d9069ec46a67896928
BLAKE2b-256 9f3324117110022cb0d47b82af5a5c6af25c0f1e083a904a2610645a7a2bb0b5

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page