Python SDK
lunixi-sdk is the Python client for Lunixi. It carries Ed25519 request signing, the token exchange, response-envelope decoding, cursor paging and a typed error family, with the fraud decision and event API on top.
- Python 3.9 or newer
- One dependency,
cryptographyfor Ed25519; HTTP uses the standard library - Ships
py.typed; the whole public interface has type hints
In this version the Python SDK covers the fraud surface and webhook verification. You can call the other endpoints through the same signed request layer, as in Extending below, or with REST without an SDK.
Installation
pip install lunixi-sdkKey pair
from lunixi import generate_key_pair
private_pem, public_pem = generate_key_pair()
# put private_pem in your secret store; upload public_pem in the dashboardRead the key from your secret store; do not put it in source code or in a file in your repository.
Creating the client
from lunixi import LunixiClient
client = LunixiClient(
base_url="https://api-gateway.lunixi.com",
key_id=key_id, # from the dashboard
private_key=private_key_pem,
)The key decides the environment. Create the client once at application start and share it: the token cache lives in this object. Optional parameters: timeout_seconds (default 30.0), max_retries (default 2), transport, token_store.
Fraud decision
from lunixi import Channel
decision = client.fraud().evaluate(
channel=Channel.STANDALONE_API,
transaction_ref="order-2026-0001",
payment={"amount": 14990, "currency": "TRY"},
customer={"id": "cus_88", "email": "[email protected]"},
)
if decision.blocked:
... # decline the transaction
elif decision.requires_three_d:
... # retry with 3D Secure
elif decision.requires_review:
... # hold it for reviewThe decision outcomes are allow, review, block and force_3d. force_3d does not decline the payment; it asks you to retry with 3D Secure. Field names are snake_case; the camelCase spelling of the API reference is accepted too. An unknown field fails locally, before the request is sent. → Fraud
| Call | Endpoint |
|---|---|
fraud().evaluate(...) | POST /api/v1/fraud/decisions/evaluate |
fraud().decisions.list(...) | GET /api/v1/fraud/decision-logs |
fraud().decisions.get(trace_id) | GET /api/v1/fraud/decision-logs/{traceId} |
fraud().events.record(event) | POST /api/v1/fraud/events/transactions |
fraud().events.record_batch(events) | POST /api/v1/fraud/events/transactions/batch |
Event submission
result = client.fraud().events.record(
{
"eventId": "evt-0001",
"eventType": "card_sale",
"subjectType": "customer",
"occurredAt": "2026-10-11T08:29:00Z",
},
idempotency_key="evt-0001",
)
if result.deduplicated:
... # this event was already recordedA batch carries at most 500 events. A batch can succeed partly; read the rejected events with failures(), fix them and send them again with the same eventId:
result = client.fraud().events.record_batch(events, idempotency_key="feed-2026-10-11-01")
for failure in result.failures():
log.warning("event %s rejected: %s", failure.event_id, failure.error)Paging
from lunixi import Decision
for row in client.fraud().decisions.iterate(decision=Decision.REVIEW, limit=50):
handle(row)The cursor is opaque: send it back as is. A broken cursor is rejected with 400; it does not silently fall back to the first page.
Webhook verification
from lunixi import FraudEvents, WebhookVerifier, WebhookVerificationError
verifier = WebhookVerifier(endpoint_secret)
try:
event = verifier.verify(raw_body, request.headers)
except WebhookVerificationError:
return 400 # never act on an unverified payload
if event.type == FraudEvents.FLOW_ASYNC_COMPLETED:
...Pass the raw body: decoding the JSON and producing it again breaks the signature. Verification is fail-closed and checks a 300-second timestamp window. While a secret is being rotated, one of the two signatures verifying is enough.
Errors
| Class | When |
|---|---|
ConfigurationError | A local value cannot be used; nothing was sent |
SignatureError | The private key could not be loaded |
ValidationError | 400 — the body was rejected |
AuthenticationError | 401 / 403 — signature, token or permission rejected |
RateLimitError | 429 — carries retry_after and quota |
ServiceUnavailableError | 503 — carries retry_after |
ApiError | Other error answers, or no answer at all |
WebhookVerificationError | An incoming webhook did not verify |
GET requests and requests you give an idempotency_key are retried after a transport error or a 5xx; evaluate is never retried on its own.
Extending
You can reach any endpoint outside the ones the SDK wraps, with signing, retries and error handling in place:
import lunixi
envelope = client.api().request("GET", "/api/v1/payments", query={"limit": 25})
rows = lunixi.items_of(envelope)Related
- Fraud — decision model and event submission
- Authentication — key, signature and token lifetime
- REST without an SDK — the signing flow