Payment Orchestrator
Linkle Ödeme
Link Oluşturma

Link Oluşturma

POST /api/v1/payments/links yeni bir ödeme linki oluşturur ve paylaşacağınız https://pay.lunixi.com/<KOD> adresini döner. Bu sayfa kimlik doğrulamayı, isteğin bütün alanlarını, yanıtı ve hataları anlatır. Linki panelden de oluşturabilirsiniz; bu sayfa sunucunuzdan yapılan API entegrasyonu içindir.

Tüm tutarlar minor-unit (kuruş) cinsinden tam sayıdır: "amountMinor": 150000 = ₺1.500,00. amountMinor, minAmountMinor, maxAmountMinor, suggestedAmountsMinor ve items[].unitPriceMinor için geçerlidir. Ondalıklı tutar göndermeyin.

Nasıl çalışır

Anahtarınıza yetki verin

API anahtarınızın Yetkiler listesinde payment-link:create (ve kullanacağınız diğer payment-link:* kodları) olmalıdır — aşağıdaki Yetkiler bölümüne bakın.

İsteği imzalayıp gönderin

Gövdeyi hazırlayın, Ed25519 anahtarınızla imzalayın ve bir Idempotency-Key ile POST /api/v1/payments/links çağırın.

Adresi paylaşın

Yanıttaki url alanını alıcınıza iletin ya da QR kodunu indirin. Link oluşturulduğu anda ACTIVE'dir (startsAt verdiyseniz o ana kadar ödeme başlatılamaz).

Ödemeyi webhook ile izleyin

Ödeme tahsil edildiğinde payment.captured gelir; paymentLinkId ile linkinize eşleştirirsiniz. → Webhook'lar

Kimlik doğrulama

Ödeme linki uçları (/api/v1/payments/links/*) yalnız API anahtarınızla imzalanmış istek kabul eder. Her istek aşağıdaki başlıkları taşır; Authorization başlığı gerekmez. Bearer erişim token'ı ya da panel oturumu bu uçlarda kimlik yerine geçmez — imza başlıkları olmayan istek 401 AUTH_HEADERS_MISSING alır.

BaşlıkDeğerZorunlu
X-Key-IdAnahtarınızın kimliği (kid) — test için mk_test_sk_…, canlı için mk_live_sk_…Evet
X-DateISO 8601 zaman damgası — sunucu saatine göre ±5 dakika içindeEvet
X-Nonceİstek başına benzersiz değer (UUID ya da rastgele hex) — tek kullanımlıktırEvet
DigestGövdenin SHA-256=<base64> hash'iGövdeli isteklerde
X-SignatureAşağıdaki kanonik dizinin Base64 Ed25519 imzasıEvet

İmzalanan metin, satırları \n ile birleştirilmiş şu yapıdır (örnekteki Digest, aşağıdaki örnek isteğin gövdesine aittir):

POST
/api/v1/payments/links
X-Date:2026-09-25T09:00:00Z
X-Nonce:64ae70b60739b3515d972c0378854a59
Digest:SHA-256=DshE/z0IEfWv/itO+8GjSJCAVm7v4gZyyw7vIML0Edo=
⚠️
  • İkinci satır tam istek yoludur ve varsa sorgu dizesini de içerir: GET /api/v1/payments/links?limit=25&state=ACTIVE imzalanırken ikinci satır /api/v1/payments/links?limit=25&state=ACTIVE olmalıdır — gönderdiğiniz sırayla ve kodlamayla.
  • Gövdesiz istekte (GET) Digest satırı hiç yazılmaz.
  • Digest'i gönderdiğiniz gövdenin kendisinden hesaplayın ve gövdeyi boşluksuz JSON olarak gönderin (JSON.stringify çıktısı gibi). Sunucu digest'i ayrıştırılmış gövdenin yeniden serileştirilmiş hâli üzerinden doğrular; girintili (pretty-printed) bir gövde 401 INVALID_DIGEST alır.

Bu bölümdeki Node.js örnekleri aşağıdaki signedHeaders yardımcısını kullanır. Özel anahtarınız sunucunuzda kalır; tarayıcıya ya da mobil uygulamaya asla konmaz.

import { createHash, createPrivateKey, sign, randomUUID } from 'node:crypto';
 
export const BASE_URL = 'https://api-gateway.lunixi.com';
const signingKey = createPrivateKey(process.env.LUNIXI_SIGNING_KEY_PEM);
 
// pathWithQuery: sorgu dizesi dahil tam yol, ör. '/api/v1/payments/links?limit=25'
// body: gönderdiğiniz JSON dizesinin kendisi; gövdesiz istekte verilmez
export function signedHeaders(method, pathWithQuery, body) {
  const date = new Date().toISOString();
  const nonce = randomUUID();
  const headers = { 'X-Key-Id': process.env.LUNIXI_KEY_ID, 'X-Date': date, 'X-Nonce': nonce };
  const lines = [method, pathWithQuery, `X-Date:${date}`, `X-Nonce:${nonce}`];
  if (body !== undefined) {
    const digest = 'SHA-256=' + createHash('sha256').update(body).digest('base64');
    headers['Content-Type'] = 'application/json';
    headers['Digest'] = digest;
    lines.push(`Digest:${digest}`);
  }
  headers['X-Signature'] = sign(null, Buffer.from(lines.join('\n')), signingKey).toString('base64');
  return headers;
}

cURL örnekleri imzayı OpenSSL 3.0 veya üstü ile hesaplar (openssl pkeyutl -rawin). Anahtar kaydı ve döndürme: Kimlik Doğrulama.

Yetkiler

İmza doğrulandıktan sonra anahtarın yetkisi (scope) kontrol edilir. Eksik yetki 403 API_INSUFFICIENT_SCOPE döner.

YetkiUçlar
payment-link:createPOST /payments/links
payment-link:viewGET /payments/links, GET /payments/links/:id, GET /:id/qr, GET /:id/attempts, GET /:id/payments
payment-link:managePATCH /payments/links/:id, POST /:id/pause, POST /:id/resume, POST /:id/deactivate
payment-link:sendPOST /payments/links/:id/send
⚠️

Mevcut anahtarlarınız bu yetkileri kendiliğinden almaz. payment:* yetkileri payment-link:* kodlarını kapsamaz. Panelde API anahtarınızın Yetkiler listesine ihtiyacınız olan payment-link:* kodlarını ekleyin ya da bu iş için yeni bir anahtar oluşturun.

Ortam

Linkin ortamını (TEST ya da LIVE) anahtarınızın ortamı belirler. Gövdede environment göndermeniz gerekmez; gönderirseniz anahtarınızın ortamıyla aynı olmalıdır, aksi hâlde 403 payment_link.environment.mismatch döner. Bir anahtar yalnız kendi ortamındaki linkleri görür ve değiştirir.

İstek

POST /api/v1/payments/links — imzalı istek, yetki payment-link:create.

Idempotency-Key

Zorunludur. 8–200 karakter, boşluksuz yazdırılabilir ASCII (^[\x21-\x7E]{8,200}$). Linki tanımlayan bir değer kullanın — ör. plink-INV-2026-0042.

  • Aynı anahtar + aynı gövde → yeni link oluşturulmaz; bu anahtarla oluşturulmuş link 200 ve replayed: true ile döner. Ağ zaman aşımında güvenle tekrar deneyebilirsiniz.
  • Aynı anahtar + farklı gövde → 409 payment_link.idempotency.conflict.
  • Anahtar yoksa 400 payment_link.idempotency.key_required, biçim dışıysa 400 payment_link.idempotency.key_invalid.

Alanlar

Tanımlanmamış bir alan ya da yanlış tip 400 VALIDATION_ERROR döner. İş kuralı ihlali (ör. FIXED linkte amountMinor yok) 422 payment_link.validation.invalid döner ve details.errors hangi alanın neden reddedildiğini listeler.

AlanTipZorunluAçıklama
environmentstringHayırTEST | LIVE. Gönderirseniz anahtarınızın ortamıyla aynı olmalı.
usagestringEvetSINGLE_USE | MULTI_USE. Değiştirilemez.
amountModestringEvetFIXED | OPEN | ITEMIZED. Değiştirilemez.
currencystring (ISO 4217)Evetör. "TRY". Hesabınızın bu ortamda tanımlı ödeme sağlayıcılarının desteklediği bir birim olmalı; aksi 422 payment_link.currency.unsupported. Değiştirilemez.
amountMinorintegerFIXED'de evetLink tutarı, > 0.
minAmountMinorintegerOPEN'da evetAlıcının girebileceği en az tutar, > 0.
maxAmountMinorintegerHayırOPEN: en çok tutar, ≥ minAmountMinor.
suggestedAmountsMinorinteger[]HayırOPEN: en fazla 6 önerilen tutar; artan sırada ve [min, max] aralığında.
quantityEnabledbooleanHayırAlıcı adet seçer. Yalnız FIXED + MULTI_USE.
minQuantity, maxQuantityintegerquantityEnabled'da evet1 ≤ minQuantity ≤ maxQuantity ≤ 999.
itemsobject[]ITEMIZED'da evet1–50 kalem — aşağıya bakın.
capacityintegerHayırYalnız MULTI_USE: satılabilecek toplam birim. Boş = sınırsız. SINGLE_USE'da gönderilirse 422.
titlestringEvet1–120 karakter, düz metin. Sayfa başlığında ve paylaşım kartında görünür.
descriptionstringHayırEn fazla 2000 karakter, düz metin — HTML ve Markdown işlenmez.
successMessagestringHayırBaşarılı ödemeden sonra gösterilen metin, en fazla 500 karakter.
successRedirectUrlstringHayırBaşarılı ödemeden sonra alıcının yönlendirileceği adres. Yalnız https://, en fazla 2048 karakter.
taxNotestringHayırTutarın yanındaki not, ör. "KDV dahil", en fazla 80 karakter.
localestringHayırSayfa dili: tr | en. Varsayılan tr.
startsAtstring (ISO 8601)HayırBu andan önce ödeme başlatılamaz. Boş = hemen.
expiresAtstring (ISO 8601)HayırLink bu anda EXPIRED olur. Gelecekte ve startsAt'ten sonra olmalı. Boş = süresiz.
eventAtstring (ISO 8601)HayırEtkinlik tarihi; yalnız sayfada gösterilir.
buttonLabelKeystringHayırÖdeme düğmesi metni: PAY | DONATE | BUY | BOOK. Boş = organizasyonunuzun varsayılanı.
requireTermsAcceptancebooleanHayırtrue ise alıcı, organizasyonunuzun yasal metin linklerini onaylamadan ödeme yapamaz.
buyerFieldsobjectHayırAlıcıdan hangi standart bilgilerin isteneceği — aşağıya bakın.
customFieldsobject[]HayırEn fazla 20 özel alan — aşağıya bakın.
recipientobjectHayırKişiye özel linkin alıcısı — aşağıya bakın.
prefillobjectHayırÖdeme kaydına yazılacak alıcı bilgileri — aşağıya bakın.
verificationobjectHayırErişim doğrulaması: { "required": true, "channel": "EMAIL" }. required: true ise recipient.email zorunludur. Yalnız EMAIL desteklenir.
remindersobjectHayır{ "enabled": true, "intervalHours": 48, "maxCount": 2 } — intervalHours 24–168, maxCount 1–3. Yalnız SINGLE_USE ve recipient doluyken.
checkoutobjectHayırÖdeme formunu daraltır — aşağıya bakın.
referencestringHayırSizin referansınız (ör. fatura no), en fazla 128 karakter. Alıcıya gösterilmez; listelemede aranabilir.
tagsstring[]HayırEn fazla 10 etiket, her biri [a-z0-9_-]{1,32}.
branchCode, salesChannel, campaign, agentCodestringHayırAtıf alanları (en fazla 64 / 32 / 64 / 32 karakter). Alıcıya gösterilmez.
merchantNotifyEmailsstring[]HayırÖdeme alındığında bildirim e-postası gidecek en fazla 5 adres.
metadataobjectHayırEn fazla 20 anahtar; anahtar [A-Za-z0-9_.-]{1,40}, değer en fazla 500 karakterlik string. payment.link.* webhook'larında ve linkin API yanıtlarında geri döner; alıcıya gösterilmez.

items[]

AlanTipZorunluAçıklama
itemKeystringHayırKalemin kimliği, [A-Za-z0-9_-]{1,64}, link içinde tekil. Boş bırakırsanız sunucu it_… biçiminde üretir. Değiştirilemez — stok sayacı bu anahtara bağlıdır.
namestringEvet1–120 karakter.
descriptionstringHayırEn fazla 500 karakter.
unitPriceMinorintegerEvetBirim fiyat, > 0.
minQuantityintegerHayır0 = alıcı bu kalemi seçmeyebilir.
maxQuantityintegerEvet1–999.
capacityintegerHayırKalem stoğu. Boş = sınırsız.

buyerFields

Her alan HIDDEN | OPTIONAL | REQUIRED değerlerinden birini alır.

AlanVarsayılanToplanan bilgi
nameREQUIREDAd ve soyad
emailREQUIREDE-posta
phoneOPTIONALTelefon (E.164)
addressHIDDENFatura adresi — adres, şehir, ülke, posta kodu
identityNumberHIDDENTC kimlik numarası (algoritmik doğrulama)

customFields[]

AlanTipAçıklama
keystring[a-z][a-z0-9_]{0,39}, link içinde tekil. Alıcının cevabı bu anahtarla döner.
labelstring1–80 karakter.
fieldTypestringTEXT | NUMBER | EMAIL | PHONE | DATE | SELECT | CHECKBOX | TCKN | VKN
requiredbooleanZorunlu alan mı.
optionsstring[]Yalnız SELECT: 1–20 seçenek, her biri 1–80 karakter.
maxLengthintegerYalnız TEXT: 1–500 (0 = 200).
helpTextstringEn fazla 200 karakter.

Cevaplar string olarak saklanır: CHECKBOX için "true" / "false", DATE için YYYY-MM-DD. Dosya yükleme alanı yoktur.

recipient ve prefill

  • recipient — { name, email, phone } (ad en fazla 120, e-posta en fazla 180 karakter, telefon E.164). Linki belirli bir kişiye gönderiyorsanız doldurun: POST /:id/send ve hatırlatmalar varsayılan olarak bu adrese gider, erişim doğrulama kodu yalnız bu e-postaya gönderilir. Ödeme sayfasında yalnız maskeli hâli görünür.
  • prefill — { name, surname, email, phone, identityNumber, address, city, country, zipCode } (country ISO 3166-1 alpha-2). Bu bilgiler alıcının tarayıcısına gönderilmez; ödeme kaydının alıcı bilgilerine sunucuda yazılır. Alıcının formda girdiği değerler yalnız boş kalan alanları doldurur. Linke erişim doğrulaması eklediyseniz, doğrulamayı geçen alıcı bu bilgileri formda görür — identityNumber hariç.

Her iki nesne de şifreli saklanır ve yalnız sizin API yanıtlarınızda açık döner.

checkout

AlanTipAçıklama
requireThreeDsbooleantrue ise bu linkteki ödemeler 3D Secure ile yapılır. Yalnız sıkılaştırır; OPEN ve MULTI_USE linklerde 3D Secure zaten zorunludur.
paymentMethodsstring[]Ödeme formunda gösterilecek yöntemler, ör. ["card"]. Panelinizdeki form ayarlarının alt kümesi olmalıdır; boş = form ayarları.

Örnek istek

# OpenSSL 3.0+ gerekir. Gövde boşluksuz JSON olmalıdır.
KEY_ID="mk_test_sk_..."             # X-Key-Id
KEY_FILE="lunixi-signing.key"       # Ed25519 özel anahtarınız (PEM)
REQ_PATH="/api/v1/payments/links"
BODY='{"usage":"SINGLE_USE","amountMode":"FIXED","currency":"TRY","amountMinor":150000,"title":"Kasım danışmanlık bedeli","expiresAt":"2026-10-31T20:59:59.000Z","reference":"INV-2026-0042","recipient":{"name":"Ayşe Yılmaz","email":"[email protected]"},"metadata":{"crmId":"A-77"}}'
 
DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
NONCE=$(openssl rand -hex 16)
DIGEST="SHA-256=$(printf '%s' "$BODY" | openssl dgst -sha256 -binary | base64)"
printf 'POST\n%s\nX-Date:%s\nX-Nonce:%s\nDigest:%s' "$REQ_PATH" "$DATE" "$NONCE" "$DIGEST" > canonical.txt
SIGNATURE=$(openssl pkeyutl -sign -inkey "$KEY_FILE" -rawin -in canonical.txt | base64 | tr -d '\n')
 
curl -X POST "https://api-gateway.lunixi.com$REQ_PATH" \
  -H "Content-Type: application/json" \
  -H "X-Key-Id: $KEY_ID" \
  -H "X-Date: $DATE" \
  -H "X-Nonce: $NONCE" \
  -H "Digest: $DIGEST" \
  -H "X-Signature: $SIGNATURE" \
  -H "Idempotency-Key: plink-INV-2026-0042" \
  --data-raw "$BODY"

Farklı link tipleri için gövde örnekleri:

// Bağış — alıcı tutarı girer, önerilen tutar düğmeleri
{ "usage": "MULTI_USE", "amountMode": "OPEN", "currency": "TRY",
  "minAmountMinor": 5000, "suggestedAmountsMinor": [10000, 25000, 50000],
  "title": "Kış kampanyası bağışı", "buttonLabelKey": "DONATE" }
 
// Bilet — sabit fiyat, adet seçimi, 200 kişilik kapasite
{ "usage": "MULTI_USE", "amountMode": "FIXED", "currency": "TRY",
  "amountMinor": 45000, "quantityEnabled": true, "minQuantity": 1, "maxQuantity": 4,
  "capacity": 200, "title": "Atölye — 12 Kasım", "eventAt": "2026-11-12T16:00:00.000Z" }
 
// Kalemli — beden seçimi, kalem başına stok
{ "usage": "MULTI_USE", "amountMode": "ITEMIZED", "currency": "TRY",
  "title": "Takım forması",
  "items": [
    { "itemKey": "forma_m", "name": "Forma — M", "unitPriceMinor": 90000, "maxQuantity": 3, "capacity": 40 },
    { "itemKey": "forma_l", "name": "Forma — L", "unitPriceMinor": 90000, "maxQuantity": 3, "capacity": 40 }
  ] }

Yanıt

201 Created — tekrar isteğinde 200 OK ve replayed: true.

{
  "status": "success",
  "code": "SUCCESS",
  "data": {
    "id": "5f0c1d2e-8a4b-4c6d-9e7f-1a2b3c4d5e6f",
    "shortCode": "7KQ2M9XAPT",
    "url": "https://pay.lunixi.com/7KQ2M9XAPT",
    "organizationId": "b1d2c3e4-…",
    "environment": "TEST",
    "state": "ACTIVE",
    "version": 1,
    "rowVersion": 1,
    "createdBy": { "type": "API_KEY", "id": "mk_test_sk_…" },
    "createdAt": "2026-09-25T09:00:00.000Z",
    "updatedAt": "2026-09-25T09:00:00.000Z",
    "completedAt": null,
    "expiredAt": null,
    "deactivatedAt": null,
    "takedown": null,
    "verificationLocked": false,
    "hasActiveAttempt": false,
    "amountLocked": false,
    "counters": {
      "paidCount": 0, "collectedAmountMinor": 0, "refundedAmountMinor": 0,
      "soldQuantity": 0, "reservedQuantity": 0, "attemptCount": 0
    },
    "availability": { "soldOut": false, "remainingQuantity": null, "inProgress": false, "items": [] },
    "usage": "SINGLE_USE",
    "amountMode": "FIXED",
    "currency": "TRY",
    "amountMinor": 150000,
    "title": "Kasım danışmanlık bedeli",
    "expiresAt": "2026-10-31T20:59:59.000Z",
    "reference": "INV-2026-0042",
    "recipient": { "name": "Ayşe Yılmaz", "email": "[email protected]", "phone": "" },
    "metadata": { "crmId": "A-77" },
    "replayed": false
  },
  "statusCode": 201,
  "requestId": "req_7f3d9c1a4b2e"
}

Yanıt, gönderdiğiniz bütün alanları varsayılanlarıyla birlikte düz olarak içerir (örnekte kısaltıldı). Ayarlanmamış zaman alanları ve isteğe bağlı kimlikler null döner. Linkin kendi alanları:

AlanAçıklama
idLinkin kimliği (UUID) — diğer bütün uçlarda :id
shortCode, url10 karakterlik kod ve paylaşılacak adres
environmentTEST | LIVE
stateACTIVE | PAUSED | COMPLETED | EXPIRED | DEACTIVATED | TAKEN_DOWN — bkz. Yaşam döngüsü
versionLinkin geçerli sürümü; her güncellemede artar
rowVersionİyimser kilit sayacı; PATCH ve durum işlemlerinde expectedRowVersion olarak geri gönderilir
createdBy{ type: "USER" | "API_KEY", id } — API ile oluşturulan linkte anahtar kimliği
completedAt, expiredAt, deactivatedAtİlgili durum geçişinin zamanı
takedownYalnız TAKEN_DOWN'da: { reasonCode, takenDownAt }; reasonCode ∈ PHISHING, FRAUD, BRAND_IMPERSONATION, PROHIBITED_GOODS, LEGAL_REQUEST, OTHER
verificationLockedErişim doğrulaması çok fazla hatalı deneme nedeniyle kilitlendi; POST /:id/send kilidi açar
hasActiveAttemptŞu anda süren bir ödeme var
amountLockedTutar ve kalemler düzenlenemez (süren ödeme var ya da ödeme alınmış)
counterspaidCount, collectedAmountMinor, refundedAmountMinor, soldQuantity, reservedQuantity (süren ödemelerin tuttuğu birim), attemptCount
availabilitysoldOut, remainingQuantity (kapasite yoksa null), inProgress (tek kullanımlıkta başka bir alıcı ödüyor), items[] (itemKey, soldOut, remainingQuantity)
replayedtrue = aynı Idempotency-Key ve aynı gövdeyle yapılan tekrar isteği; kayıtlı link döndü

Hatalar

Hata zarfı ve code üzerinden dallanma kuralı: Hata Yönetimi.

StatusKodNe yapmalı
400VALIDATION_ERRORTanımlanmamış alan ya da yanlış tip; details.errors'daki alanı düzeltin
400payment_link.idempotency.key_required / key_invalid8–200 karakterlik bir Idempotency-Key gönderin
401AUTH_HEADERS_MISSING, INVALID_DATE, NONCE_REPLAY, INVALID_KID, DIGEST_MISSING, INVALID_DIGEST, INVALID_SIGNATUREİmza başlıklarını ve kanonik diziyi kontrol edin — bkz. Kimlik doğrulama
403API_INSUFFICIENT_SCOPEAnahtarınıza payment-link:create yetkisini ekleyin
403IP_NOT_ALLOWEDİstek IP'si anahtarın izinli IP listesinde değil; panelden ekleyin
403MERCHANT_SUSPENDEDHesabınız aktif değil; destek ekibiyle iletişime geçin
403PRODUCT_NOT_ENABLEDPAYMENT_GATEWAY aboneliğiniz bu ortamda aktif değil ya da ürün API anahtarınızda tanımlı değil — bkz. PRODUCT_NOT_ENABLED
403payment_link.environment.mismatchenvironment alanını kaldırın ya da anahtarınızın ortamını gönderin
409payment_link.idempotency.conflictAynı Idempotency-Key farklı bir gövdeyle kullanıldı; yeni link için yeni anahtar üretin
409payment_link.merchant.unavailableHesabınız şu anda ödeme alamıyor (hesap ya da ürün erişimi aktif değil); destek ekibiyle iletişime geçin
422payment_link.validation.invaliddetails.errors: [{ field, reason }] listesindeki kuralları düzeltin
422payment_link.currency.unsupportedHesabınızın bu ortamda desteklediği bir para birimi seçin
503PRODUCT_ENTITLEMENT_UNAVAILABLEGeçici; aynı Idempotency-Key ile tekrar deneyin

4xx hataları aynı istekle tekrar denenerek düzelmez; önce isteği düzeltin. 5xx yanıtlarında (ör. 503, 504) isteği aynı Idempotency-Key ve aynı gövdeyle üstel geri çekilmeyle yeniden deneyin: link ilk denemede oluşturulduysa ikinci bir link açılmaz, kayıtlı link replayed: true ile döner.

Webhook'lar

Link oluşturulduğunda payment.link.created yayınlanır. Ödeme ve link event'lerinin tamamı: Webhook'lar.

Sonraki adımlar