Integrations
Elixir

Elixir SDK

Current source release: 0.1.1. The earlier 0.1.0 registry version below remains the verified published version; the new source patch is not yet claimed as published there.

The source patch adds a concrete merchant-site presentation example. Embedded checkout is the default: load only the official Lunixi bundle and mount the existing form; methods and multi-card choices stay in the Lunixi panel. Optional hosted mode uses the backend paymentPageUrl after exact HTTPS pay.lunixi.com/payment/ validation. Its token may differ from the embedded token. Missing URLs keep embedded available and hosted on hold/reconciliation with the original attempt/key. Never derive a URL from the browser token. Authorize the customer/order and native browser CSRF before rendering, send Cache-Control: private, no-store, preserve the merchant CSP nonce and keep tokens/HTML/redirect URLs out of logs. Generic response data preserves paymentPageUrl; no new typed product parity is claimed.

The :lunixi application is a server-side SDK for Elixir 1.20+ and Erlang/OTP 29+. It works with Phoenix, Plug, LiveView server processes and ordinary supervised OTP applications. OTP provides Ed25519/HMAC; Req provides verified HTTPS transport.

Install from Hex

Lunixi 0.1.0 is the last verified Hex.pm release (opens in a new tab). This older registry option is separate from the current 0.1.1 presentation example; Hex.pm 0.1.1 is not claimed available here. To pin the earlier release in your application's mix.exs:

{:lunixi, "0.1.0"}

Then run mix deps.get.

For the current embedded/hosted example, use the published v0.1.1 source tag instead of that older Hex dependency:

{:lunixi, git: "https://github.com/Lunixi-Payment/lunixi-elixir.git", tag: "v0.1.1"}

v0.1.1 source and examples (opens in a new tab). Keep mix.lock to retain the resolved source commit. The presentation example is source code to adapt into your authenticated merchant application; no public payment route is installed automatically.

For the optional native SQLite inbox, also add {:exqlite, "~> 0.43.0"}. A C compiler is needed if a precompiled NIF is unavailable. Payment/signing/webhook verification do not require a database driver.

Supervise one client per merchant

config = %Lunixi.Config{
  base_url: "https://api-gateway.lunixi.com",
  key_id: System.fetch_env!("LUNIXI_KEY_ID"),
  private_key: System.fetch_env!("LUNIXI_PRIVATE_KEY_PEM"),
  max_retries: 2
}
 
children = [{Lunixi.Client, name: MyApp.Lunixi, config: config}]
Supervisor.start_link(children, strategy: :one_for_one)

Register the Ed25519 public key in the dashboard and keep the private key on your server. PKCS8 PEM, base64 PKCS8 DER, base64 32-byte seed and validated base64 64-byte keypair formats are supported. The key selects TEST/LIVE; private credentials never belong in the browser.

Each client owns one merchant key/cache. Use distinct registered names or Registry entries for multiple merchants. Token acquisition is lazy, coalesced and renewed before expiry. Signing/token refresh are serialized in the client; protected HTTP requests run concurrently in caller processes. A supervised restart reacquires the token; tokens are not persisted.

Config/response Inspect and JSON output redact credentials/bodies; OTP client status redacts state/message data. Raw maps, explicit data accessors and process tracing remain sensitive and must not be logged or exposed. BEAM immutable memory does not provide guaranteed zeroization.

SettingDefaultBehavior
timeout30,000 msPositive, up to 120,000 ms; connect at most 10 seconds
max_retries00–5, GET or a persisted key only
max_response_bytes8 MiBAt most 32 MiB; streamed with a bound
allow_insecure_localhostfalseExplicit HTTP tests on loopback/localhost only
additional_ca_certificatesemptyTrusted DER CA certificates; verification stays on

BaseURL must be a trusted HTTPS origin without path, user information or query. Redirects and Req automatic retries are disabled. Certificate and hostname verification remain enabled. Use an application-level deadline for a workflow containing authentication/retries.

Checkout: embedded by default, optional hosted mode

Read order totals/basket from your server-side records. Save the attempt key before the API call and reuse the same key/body after an uncertain result.

alias Lunixi.Payment.{Intent, BasketItem}
 
intent = %Intent{
  amount: 10050,
  currency: "TRY",
  order_id: "ORD-1001",
  callback_url: "https://merchant.example/payment/return",
  basket_items: [
    %BasketItem{id: "line-1", name: "Example product", item_type: "PHYSICAL", price: 10050}
  ]
}
 
with {:ok, response} <- Lunixi.Payments.create_intent(MyApp.Lunixi, intent, persisted_attempt_key),
     %{"token" => token} when is_binary(token) and token != "" <- response.data do
  Lunixi.CheckoutHTML.render(token, nonce: server_generated_csp_nonce)
end

Authenticate this order's customer before returning the token/HTML, and send Cache-Control: no-store. CheckoutHTML uses only the official https://cdn.lunixi.com/lunixi-checkout.min.js, HTML-escapes token JSON, validates an optional CSP nonce and mounts the existing Lunixi form. Do not echo arbitrary returned HTML or build a custom script URL. The form handles enabled cards/alternative methods; default examples never collect raw PAN/CVV.

Money is positive integer minor units up to 9007199254740991: 10050 = 100.50 TRY. Basket price is the line total; its sum must equal root amount. Use the single root payment amount. Installment choice and fees belong to the form. Optional buyer/address and documented insurance basket details use public string-keyed schemas. Explicit false survives in preauth/force_3d; method restrictions only narrow panel-enabled methods.

A token, browser completion, HTTP success or AUTHORIZED is not proof of capture. Fulfill only after authenticating gateway state and matching stored merchant/order/payment ID, expected amount/currency and final capture/reconciliation state. Apply refund, void and dispute changes separately.

API coverage

ModuleMethods
Lunixi.Paymentscreate_intent, get, list, methods, capture, refund, void
Lunixi.Payments splitsplit, get_split, refund_split
Lunixi.Fraudevaluate, record_event, decisions, decision
Lunixi.requestOther documented merchant /api/v1/ APIs through shared signing
Lunixi.WebhookStrict raw v2 verification, rotation, freshness and ID/type binding
Lunixi.SqliteInboxOptional native SQLite transactional replay handling
Lunixi.Payments.refund(MyApp.Lunixi, payment_id, 2500, "Customer return", persisted_refund_key)

nil amount requests full capture/refund. void releases an authorization. Split checks integer amounts, 1–5 legs and exact balance; method schemas remain in the gateway. Pending split can return HTTP 400 inside a response and remains unpaid. Compensation/partial refunds/reconciliation require explicit handling.

Responses preserve envelope, data, page_info, content type, request ID and binary bytes. JSON amounts remain integers. Errors expose safe kind/code/status/request ID, without upstream messages/bodies/URLs or credentials. Explicit response data may still contain customer information and must not be logged.

Version 0.1.0 has no separate typed wallet, bank-connect, payment link, insurance, marketplace, subscription or identity modules. Follow their REST schemas through the generic transport; no typed feature parity is claimed.

Lunixi.request(MyApp.Lunixi, :get, "/api/v1/payments", query: [{"limit", "20"}])

Canonical JSON follows JavaScript numeric-index ordering, compact UTF-8 and shortest number formatting. Duplicate keys, excessive nesting and unsafe integer numbers fail before network access. Queries are encoded separately and signed; authentication has no body/Digest.

Known payment mutations require saved 16–128 character non-space printable ASCII keys. Retries keep the same key/body and use fresh nonces. Only GET/keyed operations retry configured transient failures; unkeyed mutations never automatically retry. Reconcile an uncertain result before opening another attempt.

Webhooks and durable deduplication

{:ok, event} = Lunixi.Webhook.verify(raw_body, request_headers,
  secrets: [System.fetch_env!("LUNIXI_WEBHOOK_SECRET")])

Read bounded raw bytes before JSON middleware. Pass all header pairs without combining duplicates. OTP HMAC/hash_equals authenticate the event ID, exact timestamp and raw bytes in constant time. Freshness defaults to five minutes, accepts 1–900 seconds and cannot be disabled. now is a trusted test clock, not a client timestamp. Signed body type is required; duplicate JSON/header fields and ID/type mismatches fail closed. Up to three active/previous secrets support rotation.

Verification does not consume the event. With Exqlite installed:

:ok = Lunixi.SqliteInbox.ensure_schema(connection) # startup/migration only
{:ok, applied} = Lunixi.SqliteInbox.process(connection, merchant_scope, event, fn transaction ->
  apply_verified_order_transition(transaction, event)
end)

apply_verified_order_transition is your application adapter: match financial fields and update order/outbox using this connection. Own the connection exclusively throughout the transaction; independent processes use separate connections. Keep native busy timeouts modest (the example uses 100 ms), returning a retryable HTTP failure on storage/lock errors.

Marker and business state commit together. Duplicate delivery skips the callback; changed content reusing an ID fails closed; callback/commit/storage failure rolls back the marker. Scope by merchant, retain markers for replay policy and use an outbox for external effects. Never acknowledge failed persistence with 2xx. Other databases need the same unique-event/hash + financial-update transaction contract. Process only events returned by the verifier.

The runnable receipt consumer (opens in a new tab) verifies authentic stdin/header data and records IDs/types; it does not fulfill orders. The TEST checkout example (opens in a new tab) omits tokens from output. Request a fresh provider replay for old events rather than bypassing freshness.

Verification

mix deps.get --check-locked
mix format --check-formatted
mix compile --warnings-as-errors
mix test
mix credo --strict --only warning
mix deps.audit
mix hex.audit
mix hex.build

Tests use real local TLS, generated synthetic CA certificates (OpenSSL CLI), independent Node.js JSON/Ed25519/HMAC vectors, token concurrency, malicious form input and SQLite rollback/restart/replay. All credentials are synthetic; no live merchant calls are needed. Credo runs warning/security checks; no Dialyzer proof is claimed. Security policy (opens in a new tab).

Merchant-page integration example

The source CheckoutPresentation adapter (opens in a new tab) calls the existing Lunixi.CheckoutHTML.render/2. present(response.data, nonce: nonce) defaults to {:ok, {:html, fragment}}; explicit mode: :hosted returns only a validated {:ok, {:redirect, url}}. Require this example from your authorized merchant controller and enforce native CSRF/no-store there. The existing client response already preserves paymentPageUrl; its browser and hosted tokens are independent.