Integrations
Python

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, cryptography for 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-sdk

Key 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 dashboard

Read 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 review

The 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

CallEndpoint
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 recorded

A 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

ClassWhen
ConfigurationErrorA local value cannot be used; nothing was sent
SignatureErrorThe private key could not be loaded
ValidationError400 — the body was rejected
AuthenticationError401 / 403 — signature, token or permission rejected
RateLimitError429 — carries retry_after and quota
ServiceUnavailableError503 — carries retry_after
ApiErrorOther error answers, or no answer at all
WebhookVerificationErrorAn 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