---
title: "Python SDK — Anis Developers"
description: "The Anis Partner SDK for Python 3.11 and later, synchronous and asynchronous — install, configure, call, place orders safely, handle refusals and observe."
url: https://developers.anis.ly/sdks/python/
language: en
---

> Every page of this documentation: https://developers.anis.ly/llms.txt

# Python SDK

`anis-partners` — for **Python 3.11 to 3.14**, with a synchronous and an asynchronous client that behave the same, built on `httpx` and fully typed.

- Package: [pypi.org/project/anis-partners](https://pypi.org/project/anis-partners/)
- Source code: [github.com/anis-ly/anis.partners-sdk-python](https://github.com/anis-ly/anis.partners-sdk-python)

> **An optional helper, provided as is**
>
> Every Anis SDK is provided free of charge under the MIT License, “as is”, without warranty of any kind. Anis (Aniscom for Technical Services) accepts no responsibility or liability for its use or for any loss arising from it. You remain responsible for your own integration — recording orders before you send them, recovery, key custody and testing. The source code is public: read it to understand exactly what it does before you rely on it. You do not need an SDK — you can call the Anis Partner API directly using these guides and the [API reference](https://developers.anis.ly/reference.md).

SDK **2.2.0**. SDK 1.x is deprecated; see the [changelog](https://developers.anis.ly/changelog.md).

## Install

```bash
pip install anis-partners==2.2.0
```

## Configure

```python
from anis_partners import AcceptLanguage, AnisPartnersClient, ClientOptions, PemP256Signer

options = ClientOptions(
    "https://<the address Anis gave you>",
    signature_lifetime_seconds=60,
    accept_language=AcceptLanguage.ARABIC,
    signing_key_cache_seconds=300,
    timeout_seconds=30,
)
signer = PemP256Signer.from_pem_file("/secure/partner-key.pem").for_key(key_id)

with AnisPartnersClient(options, signer) as anis:
    ...
```

For asyncio code use `AsyncAnisPartnersClient` the same way, with `async with` and `await`. The settings are checked when you build the options: an address that is not HTTPS (plain HTTP is accepted only on your own machine), or a signature lifetime outside 1 to 60 seconds, or a key cache longer than 5 minutes stops it there.

**The signer is a separate step on purpose.** `PemP256Signer` signs with a key file. For a vault or hardware module, implement `RequestSigner` — your `key_id` and a `sign(data)` method that returns the 64-byte signature — and the key never enters your process. The asynchronous client also accepts a signer whose `sign` is a coroutine, so a vault round trip never blocks the event loop.

**Several worker processes** (gunicorn, Celery, Odoo): pass `key_cache=` — an object with `get` and `set` — so workers share Anis’s public signing-key document instead of each fetching it. Use a cache only your application can write. The SDK keeps Anis’s key list for at most 5 minutes, counted from when it was fetched, even when it comes from the shared cache.

## Use it

```python
profile = anis.profile.get()

for wallet in anis.wallets.list():                                     # every page, followed for you
    print(wallet.name or wallet.id, wallet.balance)

for card in anis.catalogue.list_cards(wallet_id, subcategory_id):
    print(card.id, card.unit_price)

credential = anis.owned_cards.reveal(wallet_id, sold_card_id)
```

Amounts are `Money` values holding a `Decimal` with exactly three decimal places — never floats. `price.multiply(2)` does exact arithmetic.

## What it verifies

SDK 2.0.1 and later verify **every answer** of the calls Anis signs — place an order, read an order, reveal a card, reveal an invoice’s cards, the four enrolment calls, application suspension, key revocation and the signature self-check — success and every refusal. It requires the signature there: an answer on one of these calls without a valid signature is thrown away (`UnverifiableResponseError`) and, on an order, comes back as `OrderOutcomeUnknown`. It does not verify the information calls — your profile, wallets, the catalogue and your owned cards (list and read): their answers carry no signature, and TLS protects them. It still signs every request. See [Verifying answers](https://developers.anis.ly/docs/verifying-answers.md) for which answers are signed.

## Orders

```python
from uuid import uuid4

from anis_partners import (
    CreateOrderRequest,
    OrderCompleted,
    OrderNotPlaced,
    OrderOutcomeUnknown,
    OrderProcessing,
    OrderReplayed,
)

price = card.unit_price
if price is None:
    raise ValueError("This card has no price for this wallet.")

operation_id = uuid4()                                                # yours
request = CreateOrderRequest(card.id, quantity=2, expected_unit_price=price, expected_total=price.multiply(2))
save_order_intent(operation_id, wallet_id, request)                   # store it BEFORE sending

outcome = anis.orders.create(wallet_id=wallet_id, operation_id=operation_id, order=request)

if isinstance(outcome, OrderCompleted) and outcome.credentials:
    store_credentials(outcome.credentials)                            # store FIRST

match outcome:
    case OrderCompleted(codes_withheld=True):
        report_codes_withheld(operation_id)       # paid, codes withheld: do not buy again; write to support@anis.ly
    case OrderCompleted():
        mark_ready(operation_id)
    case OrderReplayed(order=earlier):
        ensure_stored(earlier)                    # sent earlier: reveal by invoice if you never stored them
    case OrderProcessing(retry_after=delay) | OrderOutcomeUnknown(suggested_delay=delay):
        resume_later(operation_id, delay)         # may still complete: resume with the SAME id
    case OrderNotPlaced(refusal=refusal):
        close_not_placed(operation_id, refusal)   # nothing bought: fix the cause, new order, new id
```

`anis.orders.resume(wallet_id=..., operation_id=..., order=...)` sends the same order again under the same id — the way to recover — and returns the same five outcomes. Recovery is always this signed resend, never a loop of `anis.orders.get()`, which only reads state. An order whose status is `RECOVERY_EXHAUSTED` is **not** failed: keep resuming the same id, slowly (minutes, not seconds), and tell [support@anis.ly](mailto:support@anis.ly) the operation id. In an Odoo module or any database-backed app, commit the order intent before you call Anis. See [Orders and recovery](https://developers.anis.ly/docs/orders-and-recovery.md) for the full rules.

> **No retry transport**
>
> A retrying `httpx` transport resends the same signed request with the same one-time value, and Anis refuses the copy. Recover orders with `orders.resume()`, which signs a fresh request.

## Refusals

An order does not raise a refusal: it comes back as `OrderNotPlaced` or `OrderOutcomeUnknown`. Other calls raise an `AnisApiError` (or a subclass such as `PriceChangedError` or `InsufficientBalanceError`) carrying `code`, `request_id`, `retry_after` and `is_replayed`. Each [error page](https://developers.anis.ly/errors.md) names the code.

| Exception | Means | On an order |
| --- | --- | --- |
| `UnverifiableResponseError` | An answer on a signed call could not be shown to come from Anis; it was thrown away | Not raised — `OrderOutcomeUnknown`: resume with the same id |
| `LimitExceededError` (`spending_limit_exceeded`, 429) | The order would pass the application’s or wallet’s spending cap for the current window; nothing was bought or charged | Not raised — `OrderNotPlaced`. After `retry_after`, place a new order with a new id; never resume this one |
| `RequestSigningError` | Your signer failed; nothing was sent | Raised — send it again once the signer works |
| `ValueError`, `TypeError` | Your input was wrong (an id that is not a UUID, a total that is not unit price × quantity); nothing was sent | Raised before anything is sent |

Every SDK error derives from `AnisPartnersError`, so one `except` covers them all.

## Observability

Logs go to the standard `logging` module under the `anis_partners` logger; traces and metrics go through OpenTelemetry under `anis_partners`. No key, signature, one-time value or card code is ever recorded. See [Observability](https://developers.anis.ly/docs/observability.md) for what to alert on.

## Methods and card answers

Owned-card calls use `/v1/wallets/{walletId}/owned-cards`, its `/{soldCardId}` read and `/{soldCardId}/reveal`, and `/v1/wallets/{walletId}/invoices/{invoiceId}/owned-cards/reveal`. Catalogue card reads use `GET /v1/wallets/{walletId}/catalog/cards/{cardId}`.

`CardSummary` is shared by owned cards, reveals and order `soldCards`: card id and localized name, optional face value and logo, subcategory and category. Owned cards use `soldCardId` and `operationId`. Revealed credentials inside orders include invoice id, card summary and purchase time too. Empty strings and empty localized text are omitted. Money is a string with three decimals and an uppercase three-letter currency — negative only for a wallet balance while the wallet uses allowed debt; read a wallet’s currency from `balance.currency`.

The SDK requires the v2 signing tag, both request signature headers in the covered list, and the key-document lifecycle checks. See [Verifying answers](https://developers.anis.ly/docs/verifying-answers.md). Stops are immediate; only Anis staff can resume an application, and revoking your current key requires new enrolment. See [Security](https://developers.anis.ly/docs/security.md).

```python
reason = "Suspected compromise"
catalogue_card = client.catalogue.get_card(wallet_id, card_id)
suspended = client.access.suspend_application(reason)
revoked = client.access.revoke_key(key_id, reason)
```

## Product resources in SDK 2.2.0

SDK 2.2.0 adds `meterCharges`, `directRechargeProducts` (products, locations, quote), `directRecharges` (create, a list of your own, read and recipient updates), `webhookEndpoint`, `webhookEvents`, a webhook signature verifier and updated error types. Upgrade from 2.1.0 to use them. Follow the [meter](https://developers.anis.ly/docs/meter-charges.md), [Direct recharge](https://developers.anis.ly/docs/direct-recharge.md) and [webhook](https://developers.anis.ly/docs/webhooks.md) guides for HTTP behavior. Keep the operation id and exact body before a money-moving call; a lost answer is recovered with the same id. Each SDK’s README and its docs/webhooks.md give the method names and the verifier’s arguments.
