Lunixi Python SDK
Documentation · Changelog · Issues · Security
Python client for the Lunixi platform. It carries the shared core — Ed25519 request signing, token exchange, response-envelope decoding, cursor paging and a typed error hierarchy — plus the fraud decision and event API on top of it.
Türkçe: README.tr.md
- Requires: Python 3.9 or newer
- Dependency:
cryptographyfor Ed25519. HTTP uses the standard library, so nothing else is pulled in. - Typed: ships
py.typed; every public function is annotated. - License: MIT
pip install lunixi-sdk
Quick start
from lunixi import Channel, Decision, LunixiClient
client = LunixiClient(
base_url="https://api.lunixi.com",
key_id="kid_...", # from the console
private_key=private_key_pem, # from your secret store
)
decision = client.fraud().evaluate(
channel=Channel.STANDALONE_API,
transaction_ref="order-2026-0001",
payment={"amount": "149.90", "currency": "TRY"},
customer={"id": "cus_88", "email": "buyer@example.com"},
)
if decision.blocked:
refuse_the_order(decision.reasons)
elif decision.requires_three_d:
retry_with_3d_secure()
elif decision.requires_review:
hold_for_review(decision.trace_id)
else:
authorize()
Build the client once at start-up and share it: the access-token cache lives on it, so a fresh client per request means a fresh token exchange per request.
Authentication
Two levels, and they are not interchangeable.
Token exchange. POST /api/v1/auth/token is signed with your Ed25519
private key and returns a short-lived access token. The SDK does this for you,
caches the token and refreshes it once when the gateway reports it expired.
Per-request signature. The event-ingest routes accept nothing less than a signature on every call: it is what binds an event to your account, so a body value cannot claim a different one. The SDK adds it where it is required.
The canonical string that is signed is:
<METHOD>\n<URL>\nX-Date:<value>\nX-Nonce:<value>[\nDigest:<value>]
<URL> is the path exactly as sent, including the /api/v1 prefix and any
query string. The Digest line appears only when a body is present.
Key handling
You can generate the keypair yourself and upload only the public key, so the private key never exists outside your systems:
from lunixi import generate_key_pair
private_pem, public_pem = generate_key_pair()
# store private_pem in your secret manager; upload public_pem in the console
Accepted private-key shapes: a PKCS#8 PEM block (recommended), base64 or hex PKCS#8 DER, or a bare 32-byte seed. Load it from a secret store — not from source, and not from a file inside your repository.
Fraud
The customer-facing surface is five routes:
| Call | Route |
|---|---|
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 .../events/transactions/batch |
The console's own routes are not part of this surface. They carry no stability guarantee, so they are not published here.
Decision outcomes
allow, review, block, force_3d. Note that allowed is not the
inverse of blocked: review and force_3d are neither, and force_3d asks
you to re-run the payment with 3-D Secure rather than refusing it.
Field names use Python's snake_case; the camelCase spelling from the API
reference is accepted as an alias. An unknown field raises locally and names
the key, rather than costing you a round trip to find out.
tenant_id is deliberately absent: the tenant is stamped from your access
token, and a body value is ignored.
Event ingest
Events are the history that counting rules read.
result = client.fraud().events.record(
{
"eventId": "evt-0001", # your id; a retry with the same id is deduplicated
"eventType": "card_sale",
"subjectType": "customer",
"occurredAt": "2026-10-11T08:29:00Z",
},
idempotency_key="evt-0001",
)
if result.deduplicated:
... # this event was already recorded
Batches hold up to 500 events and the SDK refuses a larger one before it reaches the network, because the gateway rejects the whole batch.
A batch can succeed partially. It answers 200 and keeps processing after
an event fails validation, so the status code alone never proves the batch
landed:
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)
Correct the body and resend a failure with the same eventId: the gateway
deduplicates, so a resend cannot double-count an event that did land.
Reading a response
Every successful response is wrapped:
{ "status": "success", "code": "SUCCESS", "data": {}, "pageInfo": {}, "requestId": "..." }
Two things to know if you ever read the envelope yourself:
- There is no
successfield. The outcome is instatus, as the string"success". Code written againstresponse["success"]readsNoneon every response. Uselunixi.is_success(response). pageInfois a sibling ofdata, not nested inside it. List endpoints returndataas a bare array, so paging metadata cannot live inside it.
The feature clients already handle both.
Paging
page = client.fraud().decisions.list(decision=Decision.REVIEW, limit=50)
while True:
for row in page.items:
handle(row)
if not page.has_more or not page.next_cursor:
break
page = client.fraud().decisions.list(
decision=Decision.REVIEW, limit=50, cursor=page.next_cursor
)
Or let the SDK follow the cursors:
for row in client.fraud().decisions.iterate(decision=Decision.REVIEW, limit=50):
handle(row)
A cursor is opaque: pass it back unchanged. Do not decode, build or edit one —
a malformed cursor is rejected with 400, it does not restart from page one.
total_count is None unless the server actually counted, because a filtered
count is expensive.
Errors
Every error derives from LunixiError.
| Class | Raised when |
|---|---|
ConfigurationError |
A local value is unusable; nothing was sent. |
SignatureError |
The private key could not be loaded. |
ValidationError |
400 — the gateway rejected the body. |
AuthenticationError |
401 / 403 — signature, token or entitlement refused. |
RateLimitError |
429 — carries retry_after and quota. |
ServiceUnavailableError |
503 — carries retry_after. |
ApiError |
Any other non-2xx answer, or no answer at all. |
WebhookVerificationError |
An inbound webhook did not verify. |
retry_after and the quota fields are None when the gateway did not send the
header. It omits one rather than guessing, and so does this SDK — an invented
number would make you retry at the wrong rate.
from lunixi import RateLimitError
try:
client.fraud().events.record(event, idempotency_key=event["eventId"])
except RateLimitError as error:
sleep(error.retry_after or 30)
Retries
A GET, and any request you give an idempotency_key, is retried after a
transport failure or a 5xx. Anything else is not: the SDK cannot tell a lost
request from a lost response, and replaying an unkeyed write can record the
same event twice.
evaluate is never retried automatically. It runs inside your payment flow, so
whether a timed-out decision is retried, treated as a soft allow, or held is
your policy to set.
Webhooks
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. The signature covers the exact bytes that arrived; parsing JSON and re-serializing it reorders keys and changes spacing, and verification then fails for every webhook. Capture the raw body before any JSON middleware touches it.
Verification is fail-closed and checks timestamp freshness within a 300-second window by default. During a secret rotation the gateway sends two signature items and either one verifying is enough, so your endpoint keeps working while you swap secrets.
The seven fraud event types are in FraudEvents; FRAUD_EVENT_TYPES holds
them all.
Extending
Any gateway route this version does not wrap is reachable with the signing, retry and error handling already in place:
envelope = client.api().request(
"GET", "/api/v1/some/other/route", query={"limit": 25}
)
rows = lunixi.items_of(envelope)
Supply your own HTTP stack (for pooling, proxies or tracing) by implementing
HttpTransport and passing transport=. In a multi-process deployment,
implement TokenStore against a shared cache and pass token_store= so the
workers share one token.
Development
python -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest
The suite runs entirely offline: it asserts on what the SDK puts on the wire through an injected transport, and verifies signing and webhook logic against committed vectors. Every key in it is synthetic and grants no access.
Samples
Runnable examples are in samples/. Each reads its settings from
the environment, so no credential is written into a file.
Metadata
Release files for lunixi-sdk 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| lunixi_sdk-0.1.0.tar.gz | 63.1 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| lunixi_sdk-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 111.1 kB
Release files / lunixi_sdk-0.1.0.tar.gz
| Download URL | lunixi_sdk-0.1.0.tar.gz |
|---|---|
| Size | 63.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8d0854f97c86df944ec457601846c286052442c8906fe505c0c01b46feae0a93
|
|
BLAKE2b-256 checksum How to use checksums |
2d3a73ceb33566e60e86fb0a4b2f3497e4312088a254ceb91b034de7689128d7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logRelease files / lunixi_sdk-0.1.0-py3-none-any.whl
| Download URL | lunixi_sdk-0.1.0-py3-none-any.whl |
|---|---|
| Size | 48.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6eaaf8a57fa87fb582b89de374baf99242bcd8b887b4bb2e7fd8ca0b114481b0
|
|
BLAKE2b-256 checksum How to use checksums |
39eba14391ae1b02d7cf4f8605fda2ea74220636b07dcb2b67005f3960459957
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logSecurity for lunixi-sdk 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for lunixi_sdk-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.