Metadata-Version: 2.4
Name: retorna-gateway-sdk
Version: 1.0.0
Summary: Retorna Gateway SDK — official Python SDK for integrating Retorna Gateway (B2B API): OAuth2 client_credentials and request signing, quotations, orders, routes and wallets behind a typed, fluent interface.
Author-email: Retorna <producto@retorna.app>
License: Retorna Gateway SDK — Proprietary License
        
        Copyright (c) 2026 Retorna. All rights reserved.
        
        1. Grant. Subject to a valid commercial agreement with Retorna and to these
           terms, Retorna grants you a limited, non-exclusive, non-transferable,
           non-sublicensable, revocable license to install and use this software (the
           "SDK") solely to integrate your systems with the Retorna Gateway services.
        
        2. Restrictions. Except as expressly permitted in writing by Retorna, you may
           not: (a) copy, modify, translate or create derivative works of the SDK;
           (b) distribute, sell, lease, lend, sublicense or otherwise make the SDK
           available to any third party; (c) reverse engineer, decompile or
           disassemble the SDK, except to the extent such restriction is prohibited
           by applicable law; (d) remove or alter any proprietary notice in the SDK;
           or (e) use the SDK to access the Retorna Gateway services other than as
           authorized by your agreement with Retorna.
        
        3. Ownership. The SDK is licensed, not sold. Retorna and its licensors retain
           all right, title and interest in and to the SDK, including all
           intellectual property rights. No rights are granted other than those
           expressly set out in Section 1.
        
        4. Termination. This license terminates automatically if you breach any of
           these terms or if your commercial agreement with Retorna ends. Upon
           termination you must stop using the SDK and destroy all copies in your
           possession.
        
        5. Disclaimer of Warranty. THE SDK IS PROVIDED "AS IS", WITHOUT WARRANTY OF
           ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
           OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
        
        6. Limitation of Liability. TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO
           EVENT SHALL RETORNA BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL,
           CONSEQUENTIAL OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS, REVENUE, DATA OR
           USE, ARISING OUT OF OR IN CONNECTION WITH THE SDK, HOWEVER CAUSED AND UNDER
           ANY THEORY OF LIABILITY.
        
        7. Entire Agreement. These terms, together with your commercial agreement with
           Retorna, constitute the entire agreement regarding the SDK. If any
           provision is held unenforceable, the remaining provisions remain in effect.
        
Project-URL: Homepage, https://github.com/retorna-tech/retorna-python-sdk-v2
Project-URL: Documentation, https://github.com/retorna-tech/retorna-python-sdk-v2#readme
Project-URL: Source, https://github.com/retorna-tech/retorna-python-sdk-v2
Project-URL: Changelog, https://github.com/retorna-tech/retorna-python-sdk-v2/blob/main/CHANGELOG.md
Keywords: retorna,gateway,b2b,payments,remittances,fintech,sdk
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=42.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Dynamic: license-file

# Retorna Gateway SDK for Python

[![PyPI](https://img.shields.io/pypi/v/retorna-gateway-sdk.svg?label=PyPI)](https://pypi.org/project/retorna-gateway-sdk/)
[![Python](https://img.shields.io/pypi/pyversions/retorna-gateway-sdk.svg)](https://pypi.org/project/retorna-gateway-sdk/)
[![License: Proprietary](https://img.shields.io/badge/License-Proprietary-lightgrey.svg)](./LICENSE)

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](https://github.com/retorna-tech/retorna-java-sdk-v2), 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 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

```bash
pip install retorna-gateway-sdk
```

```bash
uv add retorna-gateway-sdk
```

```bash
poetry add retorna-gateway-sdk
```

---

## Quickstart

The full example lives at [`examples/quickstart/quickstart.py`](./examples/quickstart/quickstart.py).

```python
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:

```python
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:

```bash
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

```python
wallet.amount          # "1234.567890"  <- str, always
float(wallet.amount)   # DON'T. IEEE-754 cannot represent 0.1.
```

When you need arithmetic, go through `Decimal`:

```python
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`:

```python
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:

```python
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:

```python
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

```bash
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](./LICENSE).
