Rewloy Python
Rewloy API'nin resmî Python kütüphanesi.
Durum: önizleme (0.x), PyPI'da yayımlandı. API kararlı; kütüphane arayüzü 1.0'a kadar değişebilir.
Rewloy, işletmelerin dijital sadakat kartlarını müşterinin telefonuna koyar. Kart türleri damga, puan, VIP, cashback, hediye kartı, kupon ve indirimdir:
- iPhone'da Apple Cüzdan;
- Android'de Rewloy Cüzdan ve Google Cüzdan;
- her yerde web kartı.
Kasada QR okutulur; bakiye, ödül ve kampanyalar kartın kendisinde güncellenir. Panelde yapılabilen her şey Rewloy API v1 ile de yapılabilir; bu kütüphane onu Python'dan kullanır. Geliştirici belgeleri: https://rewloy.com/gelistiriciler.
- Tam tipli. API'nin her işlemi,
operationIdadının snake_case hâliyle bir metottur (passAction→pass_action). İstek gövdeleri, sorgular ve yanıtlar OpenAPI belgesinden (openapi.json) üretilenTypedDicttipleriyle gelir;mypy --strictgeçer. CI belgeyi her gün okur ve değişince yeniden üretir. - Bağımlılıksız. Python 3.9 ve üstü; yalnız standart kütüphane (
urllib,json,hmac). Bağlantı havuzu ve HTTP/2 isteyen içinhttpxtaşıması isteğe bağlıdır. - Güvenli tekrar. Geçici hatalarda ölçülü yeniden deneme; satışta, kasa
işleminde ve kampanyada
Idempotency-Key. - Ötesi: sayfalama, canlı akış (SSE), webhook imzası doğrulama, kullanımdan kalkma uyarıları.
Kurulum
Python 3.9 ya da üstü gerekir:
pip install rewloy
httpx taşıması için: pip install "rewloy[httpx]".
Başlarken
import os
from rewloy import Rewloy
rewloy = Rewloy(api_key=os.environ["REWLOY_API_KEY"])
kart = rewloy.get_pass("ABCD-EFGH-JKLM")
# "Şimdi ne yapılabilir?" için `actions[].ready` okunur; `rewardReady` yalnız damga ve puanda "ödül hazır"dır.
odul = [a for a in kart["actions"] if a["action"] in ("redeem-stamps", "redeem-reward") and a["ready"]]
print(kart["type"], kart["balance"], bool(odul))
Her işlem, adı operationId'nin snake_case hâli olan bir metottur
(API referansı; OPERATION_IDS ve
METHOD_NAMES ikisini eşler). Argümanlar:
- adresteki parametreler konumsaldır:
get_pass(seri); query: sorgu parametreleri (sözlük);body: JSON gövde (sözlük);merchant:Rewloy-Merchantbaşlığı;idempotency_key:Idempotency-Keybaşlığı (satış, kasa işlemi, kampanya ve mağaza iadesinde zorunlu);timeout(saniye) vemax_retries.
Sorgu ve gövde sözlükleri API'deki adlarıyla yazılır (programId,
kvkkConsent); anahtarlar çevrilmez. Metot yanıttaki datayı döndürür:
- bir
TypedDictya da liste; yanıtı olduğu gibi alırsınız, API'nin sonradan eklediği bir alan hemen sözlüğünüzdedir; - sayfalı listelerde
Page(sayfa.data,sayfa.meta); - gövdesiz yanıtta (
204)None; - dosyada (QR, harita, CSV,
.pkpass)bytes.
Tipler rewloy.types altındadır: IssuePassBody, GetPassData,
ListCustomersItem, ErrorCode… Çalışma anında yüklenmemeleri için
(import rewloy onları yüklemez; yaklaşık 80 ms tutar) yalnız açıklamada
kullanıyorsanız TYPE_CHECKING altında içe aktarın:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from rewloy.types import IssuePassBody
Kimlik
| İstemci | Ne için |
|---|---|
Rewloy(api_key="rwk_…") |
API anahtarı: kasa, e-ticaret, kendi sisteminiz |
Rewloy(staff_session="rws_…", merchant=…) |
ekip oturumu: bir kişinin işletme uygulaması |
Rewloy(holder_session="rwh_…") |
kart sahibi oturumu: Rewloy Cüzdan gibi müşteri uygulamaları |
Rewloy() |
kimlik istemeyen uç noktalar: giriş, katılım, kod |
merchant, ekip oturumu birden fazla işletmede koltuk taşıyorsa hangi işletme
için çalıştığını söyler (Rewloy-Merchant). Her çağrıda merchant= ile
değiştirilebilir. Oturumlar kimliksiz bir istemciyle açılır:
oturum = Rewloy().login(body={"email": eposta, "password": parola})
ekip = Rewloy(staff_session=oturum["token"], merchant=isletme_id)
if oturum["mfaRequired"]:
ekip.prove_mfa(body={"code": "123456"})
Bir işlem istemcinin kimlik türünü kabul etmiyor ama kimliksiz de çalışıyorsa
(örneğin login), istemci onu kimliksiz çağırır. API, işlemin kabul etmediği
bir kimliği reddeder (CREDENTIAL_NOT_ALLOWED). Kimlik reprde görünmez.
Diğer seçenekler:
base_url(varsayılanhttps://app.rewloy.com; sonuna/v1eklemeniz ya da eklememeniz fark etmez:https://app.rewloy.com/v1de olur, kütüphane/v1i kendisi ekler);timeout(saniye; varsayılan 60,0sınırsız);max_retries(2);transport: HTTP katmanı (bkz. Taşıma);user_agent: gönderilenUser-Agenta eklenir, örneğin"KasaPOS/4.2";sleep: yeniden denemeler arasındaki beklemeyi değiştirir (testler için).
İstemci iş parçacıkları arasında paylaşılabilir. with Rewloy(...) as rewloy:
ya da rewloy.close() taşımanın tuttuklarını bırakır.
Başka bir adres (staging)
API'nin başka bir kopyasına (kendi staging ortamınız ya da bir vekil sunucu)
base_url ile bağlanılır:
rewloy = Rewloy(
api_key=os.environ["REWLOY_API_KEY"],
base_url="https://rewloy-staging.ornek.com", # sonuna /v1 yazsanız da olur
)
Gerçek müşterilere dokunmadan denemek için adres değiştirmeniz gerekmez: test modu aynı adreste, ayrı bir test ortamıyla çalışır.
Kart vermek ve kasada işlem
sonuc = rewloy.issue_pass(
body={"programId": program_id, "email": "ayse@ornek.com", "firstName": "Ayşe", "kvkkConsent": True},
)
seri, kart_adresi = sonuc["serial"], sonuc["cardUrl"]
islem = rewloy.pass_action(
seri,
body={"action": "earn-stamps", "locationId": sube_id, "count": 1},
idempotency_key=f"kasa3-z0187-fis{fis_no}", # aşağıya bakın
)
if islem.get("duplicate"):
print("Bu işlem zaten yazılmış")
Satış: record_sale
Kasa ya da kendi yazılımınız için en kolay yol record_saledir: "bu satış
oldu, sen yaz". Ödenen toplamı (kartın para biriminde, kuruş) gönderirsiniz;
ne yazılacağına kartın türü ve programın kendi kuralı karar verir. Kartın
türünü bilmeniz gerekmez.
kart = rewloy.get_pass(seri)
# Kartın türüne özgü alanlar; `balance` yerine bunları okuyun.
if "stamps" in kart:
print(f"{kart['stamps']['count']} / {kart['stamps']['max']} damga")
if "points" in kart:
print(f"{kart['points']} puan")
if "money" in kart:
print(kart["money"]["amountMinor"] / 100, kart["money"]["currency"])
musteri = kart.get("customer") # yalnız customers.read yetkisiyle; yoksa None
print(kart["programName"], musteri["name"] if musteri else None)
# Fiş numarası anahtar olamaz: kasa + Z no + fiş no, ya da satışla saklanan bir UUID.
anahtar = f"kasa3-z0187-fis{fis_no}"
satis = rewloy.record_sale(
seri,
body={
"locationId": sube_id,
"amountMinor": 4550, # 45,50: kartın para biriminde (kart["currency"]), kuruş
"currency": kart["currency"], # isteğe bağlı güvence: uyuşmazsa 422 CURRENCY_MISMATCH
"reference": f"fis-{fis_no}", # fiş numarası buraya yazılır
},
idempotency_key=anahtar,
)
if satis["applied"] == "none":
print("Yazılan bir şey yok:", satis.get("reason"))
else:
print(satis["credited"], satis["applied"], "yazıldı, bakiye", satis["balance"])
# Fişi çizmek için ayrıca okumanız gerekmez: yazımdan sonraki kart satis["card"]'dadır (yetki yoksa None).
yazimdan_sonra = satis["card"]
if yazimdan_sonra and any(a["action"] in ("redeem-stamps", "redeem-reward") and a["ready"] for a in yazimdan_sonra["actions"]):
print("Ödül hazır")
actions[].ready, kartın kendi durumuna göre işlemin şimdi yapılıp
yapılamayacağıdır (damga ödülü hazır mı, puan bir ödüle yetiyor mu, bakiye var
mı, kupon kullanılmamış mı, VIP ziyareti bu pencerede sayılmış mı). rewardReady
aynen kalır ama türe göre anlam değiştirir: damga ve puanda "ödül hazır";
cashback ve hediye kartında bakiye sıfırdan büyükse; VIP'te her zaman
True. Kasa ekranında "Ödül hazır" yazısını yalnız damga ve puanda gösterin.
GET /v1/passes/{serial} ayrıca actions (kartın aldığı kasa işlemleri ve
şimdi yapılıp yapılamayacakları) ve sale (bir satışın bu kartta ne
yazacağı) alanlarını verir.
İade. reverse_sale bir satışın karta yazdığını geri alır; satışı
yazarken gönderdiğiniz anahtarla (saleKey) ya da referencela bulur:
geri = rewloy.reverse_sale(seri, body={"saleKey": anahtar, "locationId": sube_id})
print(geri["reversed"], geri["applied"], "geri alındı, bakiye", geri["balance"])
Bir satış bir kez geri alınır (tekrar duplicate: true döner). Kazanılan
kullanılmışsa (ödüle ya da harcamaya gitmişse) 409 SALE_ALREADY_SPENT gelir ve
hiçbir şey yazılmaz.
Çevrimdışı kasa kuyruğu: occurredAt. Bağlantı koptuğunda satışı sonra
yazıyorsanız occurredAt ile satışın gerçekten olduğu anı (ISO 8601, saat
dilimiyle) gönderin; kartın geçmişinde o anla görünür. Gelecekte olamaz (2
dakikalık saat farkı kabul edilir). idempotency_key kuyruktaki kayıtla birlikte
saklanır, tekrar gönderilince satış ikinci kez yazılmaz.
rewloy.record_sale(
seri,
body={"locationId": sube_id, "amountMinor": 4550, "reference": f"fis-{fis_no}", "occurredAt": "2026-10-05T14:32:10+03:00"},
idempotency_key=anahtar,
)
Kasa işlemini iptal etmek: reverse_action. pass_action ile yapılan bir
harcama, ödül ya da kullanım yanlışlıkla yapıldıysa (spend, spend-points,
redeem-stamps, redeem-reward, use) reverse_action tamamını geri verir.
İşlemi, yaparken gönderdiğiniz Idempotency-Key (actionKey) ya da işlemin
reference değeriyle bulur (pass_action artık isteğe bağlı bir reference
alır). reverse_action bir Idempotency-Key istemez: bir işlem bir kez geri
alınır, tekrar duplicate: true döner.
rewloy.pass_action(
seri,
body={"action": "spend", "locationId": sube_id, "amountMinor": 2500},
idempotency_key=f"kasa3-z0187-iptal{fis_no}",
)
iptal = rewloy.reverse_action(seri, body={"actionKey": f"kasa3-z0187-iptal{fis_no}", "locationId": sube_id})
print(iptal["undone"], iptal["restored"], iptal["balance"], iptal["reopened"], iptal["duplicate"])
pass_actionın yanıtı kart türüne göre iki biçimdedir ve bir Union türüdür:
bakiyeli kartlarda balance (damga, puan, VIP, cashback, hediye kartı), kupon ve
indirim kartında status, uses ve usesLeft. mypy ve pyright "uses" in islem
ile ayırır. Kazanımlar (earn-stamps, earn-points, visit) reverse_actionla
değil reverse_salela geri alınır.
Yazımın yanıtında kartın durumu: card. record_sale, pass_action,
reverse_sale ve reverse_action yanıtları card taşır: yazımdan sonraki kart,
get_pass'in customer hariç aynı alanlarıyla (programName, currency,
stamps/points/money, actions…). Yazımla aynı işlemde okunur, yanıtın
balance'ıyla aynı anı söyler. Tekrarda (duplicate: True) kartın
şimdiki durumudur. Kimliğin kartın programında passes.read yetkisi yoksa
(yalnız kasa yetkisi olan bir eklenti anahtarı) card Nonedır. record_sale
yanıtındaki reversed: True, bu anahtarla yazılan satışın sonradan geri
alındığını söyler (yalnız bir tekrarda olabilir; credited ilk isteğin
yazdığıdır, kart onu artık taşımaz): fişi yeniden yazmak için yeni bir anahtar
gönderin.
Kartın işlemleri: list_pass_operations. Kartın defterindeki işlemler,
yeniden eskiye, sayfalı (rewloy.paginate("listPassOperations", path={"serial": seri})):
bir kasa ekranındaki "son işlemler" listesi ve her birinin İade düğmesi için;
kasanın kendi anahtar günlüğünü tutması gerekmez. Her işlemde undoWith hangi
uç noktanın geri aldığını ("sale/reverse" ya da "actions/reverse"),
reversible bu kimliğin şimdi geri alıp alamayacağını söyler; bu kimliğin kendi
işlemlerinde saleKey ya da actionKey de gelir.
for islem in rewloy.paginate("listPassOperations", path={"serial": seri}):
if not islem["reversible"]:
continue
if islem["undoWith"] == "sale/reverse":
rewloy.reverse_sale(seri, body={"saleKey": islem["saleKey"]})
else:
rewloy.reverse_action(seri, body={"actionKey": islem["actionKey"]})
occurredAt reddedilirse 400 VALIDATION gelir ve
err.details[0]["reason"] nedeni söyler: in_future, too_old (72 saatten
eski), before_issue (kart o anda yoktu: occurredAt olmadan yeniden
gönderin), invalid. Tanımadığınız bir reason'ı invalid gibi ele alın.
Idempotency-Key
record_sale, pass_action, send_campaign ve refund_shop_redemption bir
Idempotency-Key ister: API'nin tanımında (OpenAPI) bu başlık bu işlemlerde
zorunludur, bu yüzden idempotency_key bu metotlarda zorunlu bir anahtar
sözcük argümanıdır (vermezseniz TypeError; request() ile çağırırken
ValueError, ikisi de istek göndermeden). Kütüphane sizin yerinize anahtar
üretmez. Üretilmiş rastgele bir anahtar yalnızca tek çağrının yeniden
denemelerini korurdu: uygulama çöküp yeniden başlarsa yeni bir anahtar çıkar ve
satış ikinci kez yazılabilirdi. Anahtarı kendiniz üretip satışla birlikte
saklayın. Anahtar 8–64 karakterlik görünür ASCII olmalıdır (0x21–0x7E: harf,
rakam ve noktalama; boşluk, Türkçe harf ya da fiş gibi ASCII dışı karakter
olmaz); aksi halde kütüphane yine istek göndermeden ValueError fırlatır.
Başlığın isteğe bağlı olduğu işlemlerde (örneğin issue_pass) anahtar
verilmezse kütüphane bir UUID üretir ve aynı çağrının her denemesinde aynısını
gönderir.
- Anahtar bir kimlik için kalıcı olarak tekildir (8–64 karakter; defterden
hiç silinmez). Aynı anahtarla aynı isteğin tekrarı ikinci kez yazmaz ve
ilk sonucu
duplicate: trueile döndürür. Aynı anahtar başka bir gövdeyle422 IDEMPOTENCY_KEY_REUSEDalır. - Fiş numarası tek başına anahtar olamaz: yazarkasa fiş numaraları Z
raporundan sonra yeniden başlar. Kasa + Z no + fiş no birleşimi
(
kasa3-z0187-fis0042) ya da satışla birlikte saklanıp tekrarda yeniden gönderilen bir UUID kullanın. - Fiş numarası
referencealanına yazılır; müşterinin geçmişinde ve işlem dökümünde görünür.
Sayfalama
for musteri in rewloy.paginate("listCustomers", query={"consent": "yes", "limit": 200}):
print(musteri["displayName"], musteri["identifiers"])
paginate sayfalı her listeyi (page/limit ve meta) öğe öğe dolaşır ve
son sayfada durur; tembeldir, bıraktığınız yerde istek de durur. Adreste
parametresi olan listelere path={"id": …} verilir. Tek bir sayfa için
metodun kendisi yeter: sayfa = rewloy.list_customers(query={"page": 2})
(sayfa.data, sayfa.meta).
Canlı akış
with rewloy.live_feed() as akis:
for olay in akis:
if olay.event == "event":
ev = olay.json()
print(ev["kind"], ev["location"], ev["program"], ev["delta"], ev["unit"], ev["name"])
live_feed (işletmenin tezgâh akışı) ve holder_card_events (kart sahibinin
kartındaki değişiklik) sunucu olayları (text/event-stream) yayınlar.
rewloy.stream("liveFeed", …) aynı işi görür. Her olay event, data ve
id taşır; json() datayı ayrıştırır.
- Yeniden bağlanma. Bağlantı koparsa akış kendiliğinden yeniden bağlanır:
sunucunun
retry:süresi kadar bekler, bir olayidtaşıdıysaLast-Event-IDgönderir.reconnect=Falsebunu kapatır. - Sessiz bağlantı. API 25 saniyede bir
: hbgönderir; 60 saniye hiç veri gelmezse bağlantı kopmuş sayılır (idle_timeout=). - Durdurmak: döngüden
break(bağlantı hemen kapanır),withbloğundan çıkmak ya da başka bir iş parçacığındanakis.close(). - Bitiren hatalar. Yeniden bağlanmanın düzeltemeyeceği bir hata (
401,403,404) akışıRewloyErrorile bitirir.
Webhook doğrulama
Rewloy her teslimi imzalar:
Rewloy-Signature: t=<unix saniye>,v1=<hex HMAC-SHA256(sır, "<t>.<ham gövde>")>
verify_webhook imzayı ham gövdeyle ve webhook oluşturulurken bir kez
gösterilen sırla (whsec_…) doğrular:
- karşılaştırmayı
hmac.compare_digestile sabit sürede yapar; tşimdiden 300 saniyeden (tolerance=) uzaksa reddeder;- gövdeyi ayrıştırılmış olarak (
dict) döndürür.
Webhook'u panelden ya da API'den ekleyebilirsiniz. webhooks.manage yetkili
bir API anahtarı create_webhook, list_webhooks, get_webhook,
set_webhook_status, test_webhook ve list_webhook_deliveriesi çağırabilir;
webhook_events abone olunabilecek olayları söyler. Sır (secret) yalnız
create_webhook yanıtında gelir, saklayın:
yeni = rewloy.create_webhook(
body={"url": "https://ornek.com/rewloy/webhook", "events": ["pass.activity", "pass.voided"]},
)
sir = yeni["secret"]
rewloy.test_webhook(yeni["webhook"]["id"]) # webhook.test olayı gönderir
Adres herkese açık bir https adresi olmalıdır (test ortamında da);
yerelde bir tünel kullanın.
Sırrı yenilemek. Kaybolan ya da sızan bir sır için rotate_webhook_secret
webhook'a yeni bir sır verir (yeni secret yalnız o yanıtta döner); webhook'u
silip yeniden eklemek gerekmez. Eski sır 24 saat daha yeninin yanında imzalar:
o sürede Rewloy-Signature iki v1 taşır ve teslimler
Rewloy-Signature-Rotating: 1 başlığıyla gelir. verify_webhook her v1'i ve
secret olarak verilen birden çok sırrı dener; yenilemeden önce alıcınızı
[yeni, eski] ile güncelleyin. delete_webhook webhook'u teslim geçmişiyle
birlikte kalıcı siler (204).
yeni = rewloy.rotate_webhook_secret(webhook_id)["secret"]
# yeni sırrı alıcınıza ekleyin, 24 saat sonra eskisini bırakın
olay = verify_webhook(ham_govde, imza_basligi, [yeni, eski_sir])
Tutmazsa WebhookSignatureError atar: 400 ile yanıtlayın ve hiçbir işlem
yapmayın. Gövde mutlaka ham olmalıdır (str ya da bytes). JSON olarak
ayrıştırılıp yeniden yazılan bir gövde imzayı tutturmaz; ayrıştırılmış bir
dict verirseniz TypeError alırsınız.
Flask:
import os
from flask import Flask, request
from rewloy import WebhookSignatureError, verify_webhook
app = Flask(__name__)
@app.post("/rewloy/webhook")
def rewloy_webhook():
try:
olay = verify_webhook(
request.get_data(),
request.headers.get("Rewloy-Signature"),
os.environ["REWLOY_WEBHOOK_SECRET"],
)
except WebhookSignatureError:
return "", 400
# Rewloy-Delivery bir teslimin her denemesinde aynıdır: işlediyseniz atlayın.
if daha_once_islendi(request.headers.get("Rewloy-Delivery")):
return "", 200
if olay["type"] == "pass.activity":
print(olay["data"]["card"], olay["data"]["kind"], olay["data"]["delta"])
return "", 200
FastAPI (Django'da ham gövde request.bodydir):
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
@app.post("/rewloy/webhook")
async def rewloy_webhook(request: Request) -> dict[str, bool]:
try:
olay = verify_webhook(await request.body(), request.headers.get("rewloy-signature"), SIR)
except WebhookSignatureError:
raise HTTPException(status_code=400)
...
return {"ok": True}
Başlıklar:
Rewloy-Event: olay türü (pass.issued,pass.activity,pass.voided,webhook.test); gövdedekitypeile aynı.Rewloy-Delivery: teslimin kimliği. Teslim "en az bir kez"dir: çift gelen teslimi bununla ayıklayın.
Gövde kişinin iletişim bilgisini taşımaz; kişiyi customer_id ile API'den
okuyun. 2xx dışı bir yanıt yaklaşık 45 saat boyunca 8 kez yeniden denenir ve
her deneme yeni bir t ile imzalanır. Sonuç PassEvent ya da
WebhookTestEvent tipindedir: olay["type"] ile mypy türü daraltır, bilinmeyen
yeni bir tür için bir else dalı bırakın. Kendi işleyicinizi test etmek için
sign_webhook(govde, sir) aynı başlığı üretir.
Webhook'un durumu. Webhook nesnesinde (list_webhooks, get_webhook,
set_webhook_status, create_webhook ve rotate_webhook_secret'ın webhook'u)
iki tarih alanı hep vardır, ikisi de boş olabilir (str | None, bir tarih):
pausedUntil: alıcınız art arda iki kez5xx,429verdi ya da yanıt vermedi; açık webhook'un teslimleri bu ana kadar bekler, sonra kendiliğinden yeniden denenir (60 saniye). Bekletilmiyorsa ya da webhook kapalıysa boştur.resumableUntil: webhook'u kurallar kapattı ve bekleyen teslimleri saklanıyor (kapanıştan 24 saat sonrasına kadar). Bu andan önceset_webhook_status(id, body={"active": True})ile açarsanız kaldığı yerden devam eder: saklananlar hemen gider, kapalıyken olan olaylar da gelir. Açıksa, bir kişi ya da anahtar kapattıysa ya da süre geçtiyse boştur.
for w in rewloy.list_webhooks():
if w["pausedUntil"]:
print(f'{w["url"]}: {w["pausedUntil"]} anına kadar bekletiliyor')
if w["resumableUntil"]:
print(f'{w["url"]}: {w["resumableUntil"]} öncesinde açın, kaldığı yerden sürer')
Hatalar ve yeniden deneme
from rewloy import RateLimitError, RewloyError
try:
rewloy.pass_action(
seri,
body={"action": "spend", "locationId": sube_id, "amountMinor": 5000},
idempotency_key=f"kasa3-z0187-fis{fis_no}",
)
except RateLimitError as err:
print(f"{err.retry_after} saniye sonra yeniden deneyin")
except RewloyError as err:
if err.code == "INSUFFICIENT_BALANCE":
print(err.detail)
else:
raise
RewloyError şunları taşır:
status: HTTP durumu;code: API'nin sabit kodu (hata kodları); kodunuz buna göre davranmalı (rewloy.types.ErrorCodebugünkü kodları sayar);title: kodun katalogdaki başlığı;detail: API'nin açıklaması (Türkçe, değişebilir);details: varsa ayrıntı; doğrulama hatasında[{"field", "rule", "message"}];request_id:x-request-id; destek talebinde bunu verin;body,headers,docsveoperation.
Alt sınıflar:
RateLimitError:429;retry_aftersaniye. Her hata (bu dahil) yanıtınRateLimit-*başlıklarınıerr.rate_limitolarak taşır (RateLimit(limit, remaining, reset); başlık yoksaNone);RewloyConnectionError: yanıt gelmedi (status0,codeCONNECTION_ERROR);RewloyTimeoutError: zaman aşımı (TIMEOUT).
Rewloy'un olmayan bir hata gövdesi (örneğin bir vekil sunucunun 502 sayfası)
HTTP_502 gibi bir kodla gelir. Yanlış kullanım (yanlış önekli bir anahtar,
eksik adres parametresi) Python'un ValueErrorı ya da TypeErrorıdır.
Yeniden deneme. Şunlar en çok max_retries kez (varsayılan 2) yeniden
denenir: bağlantı hatası, zaman aşımı, 429, 502, 503, 504 ve
Cloudflare'in 520–524 hataları.
- Bekleme: üstel ve rastgele (0,5 sn, 1 sn, 2 sn… en çok 8 sn); yanıt
Retry-Aftertaşıyorsa o kadar.Retry-After60 saniyeden uzunsa beklenmez, hata size gelir. - Yalnız tekrarı güvenli istekler:
GET,PUT,DELETEveIdempotency-KeytaşıyanPOST. İlk istek hâlâ işlenirken gelen409 IDEMPOTENCY_IN_PROGRESSde beklenip yeniden denenir. DiğerPOSTvePATCHistekleri hiç tekrar edilmez. - Süre: her deneme
timeout(varsayılan 60 sn) içinde bitmelidir; süre gövdenin tamamını kapsar.
Kullanımdan kalkma
Kalkacak bir uç nokta en az 180 gün önceden duyurulur. O süre boyunca her
yanıtı Deprecation, Sunset ve Link başlıklarını taşır.
- Uyarı. Kütüphane her işlem için bir kez
warnings.warnile birDeprecationWarningyayar. Uyarı işlemi,Sunsettarihini ve değişiklik günlüğündeki kaydı söyler; satır olarak sizin çağrınızı gösterir, bu yüzden Python'un varsayılan süzgeci onu__main__kodunda gösterir. - Tipler. O metodun belge dizgisi (docstring) kaldırılacağını söyler; kalkacak yanıt alanları da metodun belgesinde ve tiplerde işaretlidir.
- Yönetmek.
python -W defaulther yerde gösterir;-W ignore::DeprecationWarningya dawarnings.filterwarningssusturur.-W errorile uyarı istisna olurdu ama sunucu işi yapmış olurdu ve yanıt kaybolurdu: bu yüzden çağrı döner ve uyarırewloykaydedicisine (logging) yazılır.
Yanıtın tamamı ve test modu
yanit = rewloy.request(
"sendCampaign",
body={"body": "Bu hafta kahveler 2 damga!"},
idempotency_key="kampanya-2026-10-03",
)
yanit.status # 201
yanit.replayed # True: aynı anahtarın ilk yanıtı yeniden döndü (Idempotent-Replayed)
yanit.request_id # x-request-id
yanit.rate_limit # RateLimit(limit=120, remaining=117, reset=41): RateLimit-* başlıkları, yoksa None
yanit.mode # Rewloy-Mode
yanit.data # kampanya
request(işlem, …) her işlemi çağırır (operationId ya da metot adıyla) ve
yanıtın tamamını döndürür: data, sayfalı listede meta, status,
headers, request_id, rate_limit, mode ve replayed. Adres parametreleri
path={"serial": …} ile verilir. data burada tipli değildir; typing.cast
ya da metodun kendisi.
mode, yanıtın Rewloy-Mode başlığıdır: live ya da test. Başlık yoksa
None. Canlı akışta aynı bilgi akis.modedadır.
Test modu
Gerçek müşterilere dokunmadan denemek için işletmenizin bir test ortamı
vardır: ona bağlı ayrı bir işletme (adı "· Test" ile biter); kendi
programları, müşterileri, kartları, anahtarları ve webhook'ları. Panel →
Geliştirici → "Test ortamını aç" ya da POST /v1/test/environment. Orada
oluşturulan anahtar rwk_test_ ile başlar ve aynı adreste, aynı yollarla
çalışır:
rewloy = Rewloy(api_key=os.environ["REWLOY_TEST_KEY"]) # rwk_test_…
yanit = rewloy.request("getPass", path={"serial": seri})
yanit.mode # "test"
- Test ortamı hiçbir şey göndermez (e-posta, bildirim, SMS); kartlar
cüzdanlara eklenmez. Gönderilmeyenler
GET /v1/test/messagesile okunur. - Webhook'lar teslim edilir ve
Rewloy-Test: 1başlığıyla"test": truetaşır. - Gerçek müşteri verisini test ortamına girmeyin.
reset_test_environment(1.2.0'dan beri) müşterileri, kartları, kodları ve kayıtları siler; ortamın kimliği, programları, şubeleri, anahtarları ve webhook'ları kalır, entegrasyonunuz aynı anahtarla sürer. Bir anahtar sızdıysabody={"revokeKeys": True}anahtarları da geçersiz kılar ve webhook'ları kapatır. Yanıtdeletedvekeptsayılarını verir;closedartık hepNonedır.- POS için anahtar:
create_api_key(body={"kind": "pos", "locationId": sube_id, "register": "Kasa 1", "password": sifre})hazır Kasa rolüyle yalnız o şubede çalışan bir anahtar oluşturur; yanıttakibaseUrlPOS'a yazılacak adrestir. list_all_batchesişletmenin bütün hediye kartı, kupon ve indirim kodlarını sayfalar (statussüzgeci:open,full,expired,closedya daarchived; satırınstate'i de bunlardan biri:archivedkodun programı arşivde demektir, bağlantısı kart vermez). Arşivdeki bir programa kod oluşturmak409 PROGRAM_ARCHIVEDverir.send_batch_linkkodun bağlantısını yalnız kod kart verirken e-postayla gönderir: durdurulmuş kod410 BATCH_CLOSED, süresi dolmuş410 BATCH_EXPIRED, kartları bitmiş410 BATCH_FULL, programı arşivde olan409 PROGRAM_ARCHIVEDverir ve e-posta gitmez (1.2.0'dan önce son üçünde de giderdi). Kodlarırewloy.types.ErrorCodedeğerleri içindedir.
Ayrıntı: https://rewloy.com/gelistiriciler#test-ortamı
İşlem tablosu da dışa açıktır: OPERATIONS["passAction"] →
OperationMeta(id, method_name, http_method, path, auth, merchant, idempotency, body, response, paged, stream, deprecated).
Taşıma ve test etmek
Varsayılan taşıma urllibdir: bağımlılık yok, yönlendirme izlenmez (API
yönlendirmez; izlemek anahtarı başka yere taşıyabilir), yalnız http ve
https, ortam değişkenlerindeki vekil (HTTPS_PROXY) kullanılır. Her istek
yeni bir bağlantı açar. Bağlantı havuzu, HTTP/2 ya da kendi vekil ve sertifika
ayarlarınız için:
import httpx
from rewloy import Rewloy
from rewloy.httpx_transport import HttpxTransport # pip install "rewloy[httpx]"
rewloy = Rewloy(api_key=anahtar, transport=HttpxTransport(httpx.Client(http2=True)))
transport= aynı zamanda sahte bir API'dir: send(request), open_stream(request)
ve close() olan her nesne olur. Kendi kodunuzu ağsız test etmek için:
from rewloy import Headers, HttpRequest, HttpResponse, Rewloy
class SahteTasima:
def send(self, request: HttpRequest) -> HttpResponse:
return HttpResponse(200, "OK", Headers([("Content-Type", "application/json")]),
b'{"data": {"serial": "ABCD-EFGH-JKLM", "balance": 3}}')
def open_stream(self, request: HttpRequest): raise NotImplementedError
def close(self) -> None: pass
rewloy = Rewloy(api_key="rwk_test", transport=SahteTasima())
assert rewloy.get_pass("ABCD-EFGH-JKLM")["balance"] == 3
asyncio
İstemci eşzamanlıdır (decision 18: docs/DECISIONS.md). Bir
asyncio uygulamasında iş parçacığına verin; istemci iş parçacığı güvenlidir:
kart = await asyncio.to_thread(rewloy.get_pass, "ABCD-EFGH-JKLM")
Geliştirme
python3 -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
python scripts/generate.py # canlı belgeden: openapi/openapi.json ve src/rewloy/generated/
python scripts/generate.py --file openapi/openapi.json # kayıtlı belgeden
mypy && pytest
src/rewloy/generated/elle düzenlenmez; üreteçscripts/generator.py'dir.- Testler ağa çıkmaz: yerel bir sahte API (
http.server) ile çalışır. - CI her gün canlı belgeyi okur ve bir değişiklik varsa bir pull request açar.
- Kararlar: docs/DECISIONS.md.
Belgeler
| Başlarken | https://rewloy.com/gelistiriciler |
| API referansı | https://rewloy.com/gelistiriciler/api |
| OpenAPI 3.1 | https://app.rewloy.com/v1/openapi.json |
| Hata kodları | https://rewloy.com/gelistiriciler/hatalar |
| API'nin değişiklik günlüğü | https://rewloy.com/gelistiriciler/degisiklikler |
| Bu kütüphanenin değişiklikleri | CHANGELOG.md |
Sürümler:
- Kütüphane anlamsal sürümleme (SemVer) kullanır. 1.0'a kadar arayüzü değişebilir.
- API'ye alan eklemek geriye uyumludur; kütüphanenin tipleri her gün güncellenir.
- Kalkacak bir uç nokta en az 180 gün önce duyurulur ve bu süre boyunca
DeprecationveSunsetbaşlıklarını taşır.
Güvenlik
Bir güvenlik açığı bulursanız SECURITY.md dosyasındaki yoldan özel olarak bildirin. Lütfen herkese açık issue açmayın.
Lisans
English
Developer docs (in Turkish): https://rewloy.com/gelistiriciler.
The official Python library for the Rewloy API.
Status: preview (0.x), published on PyPI. The API is stable; the library's interface may change until 1.0.
The documentation of the API itself is in Turkish (links above). In short:
- Every operation of the API is a method named by its operationId in
snake_case (
passActionispass_action), typed withTypedDicts from the OpenAPI document, which CI reads daily and regenerates from.mypy --strictpasses. - No dependencies: Python 3.9 or later and the standard library (
urllib,json,hmac). An optionalhttpxtransport adds pooling and HTTP/2. - Safe retries,
Idempotency-Keyhandling, pagination, server-sent events, webhook signature verification and deprecation warnings.
Install
Python 3.9 or later:
pip install rewloy
Use
import os
from rewloy import Rewloy
rewloy = Rewloy(api_key=os.environ["REWLOY_API_KEY"]) # or staff_session=…, merchant=… or holder_session=…
created = rewloy.issue_pass(body={"programId": program_id, "email": email, "kvkkConsent": True})
sale = rewloy.record_sale(
created["serial"],
body={"locationId": location_id, "amountMinor": 4550, "reference": f"receipt-{receipt_no}"}, # amount in the card's currency, minor units
idempotency_key=f"till3-z0187-r{receipt_no}",
)
# A gift-card spend rung up by mistake? Void it by the key it was sent with:
rewloy.pass_action(
created["serial"],
body={"action": "spend", "locationId": location_id, "amountMinor": 2500},
idempotency_key=f"till3-z0187-s{receipt_no}",
)
voided = rewloy.reverse_action(created["serial"], body={"actionKey": f"till3-z0187-s{receipt_no}"})
print(voided["undone"], voided["restored"], voided["balance"]) # 'spend', 2500, the balance again
- Till.
record_salewrites a completed sale to a card (the card type and the programme's own rule decide what is written);get_passreturns the card's structured fields (programName,currency,stamps,points,money,customer);reverse_saletakes a refunded sale back:rewloy.reverse_sale(serial, body={"saleKey": key}). A void isreverse_action: it takes back apass_actionthat was a mistake (spend,spend-points,redeem-stamps,redeem-reward,use), found by theIdempotency-Keyyou sent with it (actionKey) or itsreference; it needs noIdempotency-Keyof its own, and a repeat answersduplicate: True:rewloy.reverse_action(serial, body={"actionKey": key}). A till that queues sales while offline sendsoccurredAt(ISO 8601 with the UTC offset, not in the future) withrecord_sale, so the card's history shows when the sale really happened; the queuedidempotency_keymakes the resend safe.pass_actiontakes an optionalreferencetoo, and its answer is aUnionof twoTypedDicts: the balance-card answer (balance) or the coupon / discount-card answer (status,uses,usesLeft);"uses" in answernarrows it for mypy and pyright. cardon write answers.record_sale,pass_action,reverse_saleandreverse_actionanswer withcard: the card after the write, the fields ofget_passexceptcustomer, read in the same transaction (on a replay,duplicate: True, it is the card's current state). A key withoutpasses.readin the card's programme getscard: None.record_sale'sreversed: True(replays only) says the sale written under that key was taken back since: send a new key to write the receipt again. For "can I act now" readcard["actions"][i]["ready"];rewardReadymeans "reward ready" only for stamp and points cards (alwaysTrueon VIP, any balance on cashback and gift cards).- Recent operations.
list_pass_operationslists a card's ledger operations, newest first and paged, for a till's "last operations" screen:undoWith("sale/reverse"or"actions/reverse"),reversibleand, for this credential's own operations,saleKey/actionKeyto pass straight toreverse_sale/reverse_action. - Rejected
occurredAtis a400 VALIDATIONwhoseerr.details[0]["reason"]isin_future,too_old,before_issueorinvalid(treat an unknown reason asinvalid). - Idempotency keys.
record_sale,pass_action,send_campaignandrefund_shop_redemptionneed anIdempotency-Key: the API's OpenAPI document marks the header required for them, soidempotency_keyis a required keyword argument (leave it out and you get aTypeError, or aValueErrorthroughrequest(), before anything is sent). The client never makes one up for you (a generated key would not survive a restart of your app). The key must be 8–64 printable ASCII characters (0x21–0x7E); a non-ASCII key such asfiş-0042is refused client-side with aValueErrorbefore anything is sent. Where the header is optional (for exampleissue_pass) the client still generates a UUID and reuses it on every retry of the call. A key is unique for good per credential: do not use the receipt number alone (fiscal receipt numbers restart after the Z report) but register + Z number + receipt number, or a UUID stored with the sale. The receipt number goes inreference. - Base URL.
Rewloy(api_key=key, base_url="https://staging.example.com")orbase_url="https://staging.example.com/v1": with or without a trailing/v1(and trailing slashes), the client appends/v1/...itself. Defaulthttps://app.rewloy.com. - Test mode. Open the test environment (panel → Developer, or
POST /v1/test/environment) and use itsrwk_test_key at the same address: a separate test business that sends nothing and never reaches real customers. Webhooks are delivered withRewloy-Test: 1. - Arguments. Path parameters are positional.
query,body,merchant,idempotency_key,timeout(seconds) andmax_retriesare keywords. The dicts use the API's own key names. - Results. A method returns the answer's
data: aTypedDictor list, aPage(.data,.meta) for paged lists,Nonefor 204,bytesfor files. Types are inrewloy.types(from rewloy.types import IssuePassBody). - The whole answer.
rewloy.request("sendCampaign", body=…)returnsstatus,headers,request_id,rate_limit(RateLimit(limit, remaining, reset)from theRateLimit-*headers,Nonewhen absent),mode(theRewloy-Modeheader:liveortest) andreplayed(Idempotent-Replayed) withdata. - Pagination.
rewloy.paginate("listCustomers", query=…)iterates the items of every page, lazily. - Streams.
with rewloy.live_feed() as stream: for event in stream: …iterates server-sent events (event,data,id,json()). It reconnects withLast-Event-IDunlessreconnect=False;close()works from another thread. - Threads and asyncio. The client is thread-safe. It is synchronous:
in async code use
await asyncio.to_thread(rewloy.get_pass, serial). - Testing.
transport=takes anything withsend(),open_stream()andclose(); the README above has a fake.
Webhooks
Verify the raw body (request.get_data() in Flask, await request.body()
in FastAPI, request.body in Django) with the secret shown when the webhook
was created:
event = verify_webhook(raw_body, headers.get("Rewloy-Signature"), secret)
- Check.
Rewloy-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>is compared withhmac.compare_digest, andtmust be within 300 seconds. - Refusal. On failure it raises
WebhookSignatureError: answer 400. - Headers.
Rewloy-Eventis the event type.Rewloy-Deliveryis the same on every retry of a delivery: deduplicate on it. Delivery is at least once.
rotate_webhook_secret gives a webhook a new secret (returned only in that
answer); the old one keeps signing for 24 hours, so Rewloy-Signature carries
two v1 values and the delivery has Rewloy-Signature-Rotating: 1.
verify_webhook tries every v1 and every secret you pass:
[new_secret, old_secret]. delete_webhook removes a webhook and its delivery
history for good.
A webhook object (list_webhooks, get_webhook, set_webhook_status, and the
webhook of create_webhook and rotate_webhook_secret) always carries two
fields, each str | None (a date-time), null when it does not apply:
pausedUntil: your receiver failed twice in a row (5xx,429, a connection error or no answer), so the open webhook's deliveries wait until this moment and are then retried on their own (60 seconds). Null when it is not paused or the webhook is off.resumableUntil: the rules turned the webhook off and its pending deliveries are kept (until 24 hours after it closed). Turn it on before this moment (set_webhook_status(id, body={"active": True})) and it carries on where it stopped: the kept deliveries go at once and the events that happened meanwhile arrive too. Null while it is on, when a person or a key turned it off, or once the time has passed.
Also in Rewloy 1.2.0 (library 0.2.4): create_api_key(body={"kind": "pos", "locationId": …, "register": …, "password": …})
(a till key bound to one branch); reset_test_environment(body={"revokeKeys": True})
(keeps the test business, programmes and keys; revokes keys only when asked);
list_all_batches (every gift-card, coupon and discount code of the business,
with the archived state); 409 PROGRAM_ARCHIVED when creating a code for an
archived programme.
send_batch_link e-mails a code's link only while the code issues a card:
410 BATCH_CLOSED (stopped), 410 BATCH_EXPIRED (past its date),
410 BATCH_FULL (every card given) and 409 PROGRAM_ARCHIVED (its programme is
archived) refuse it and no mail goes; before 1.2.0 the last three were sent
anyway. The codes are in the rewloy.types.ErrorCode values.
Errors, retries, deprecations
- Errors. Failures raise
RewloyErrorwithstatus,code(the API's stable code),title,detail,details,request_id,rate_limitandbody. Subclasses:RateLimitError(retry_after),RewloyConnectionErrorandRewloyTimeoutError. Misuse raisesValueErrororTypeError. - What is retried. Network errors, timeouts, 429, 502–504 and
Cloudflare's 520–524, up to
max_retries(default 2), with exponential backoff and jitter, honouringRetry-After. The timeout covers a whole attempt, body included. - Only when safe. Only GET, PUT, DELETE, and POST with an
Idempotency-Key, are retried. - Deprecations. A deprecated operation's answers carry
Deprecation,SunsetandLink. The client issues oneDeprecationWarningper operation, attributed to your calling line. Under-W errorthe call still returns and the notice goes to therewloylogger.
Security and licence
Report vulnerabilities privately, as SECURITY.md says. MIT licensed.
Yeni sürüm yayımlamak / Releasing
src/rewloy/_version.py'deki sürümü ve CHANGELOG'u güncelleyin, commit'leyin, v<sürüm> etiketini gönderin. release.yml PyPI'a güvenilir yayıncı (trusted publishing) yoluyla, jetonsuz yayımlar.
Bump the version in src/rewloy/_version.py and the changelog, commit, and push a v<version> tag. release.yml publishes to PyPI through trusted publishing, with no token.
Metadata
Release files for rewloy 0.2.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| rewloy-0.2.4.tar.gz | 514.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rewloy-0.2.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 696.3 kB
Release files / rewloy-0.2.4.tar.gz
| Download URL | rewloy-0.2.4.tar.gz |
|---|---|
| Size | 514.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c943f46197093c0b3b1c5f174c1da2f4fe4c99129649e70a6aa3472f2f894e3f
|
|
BLAKE2b-256 checksum How to use checksums |
8cee606c682306a6d0432e684b0c83086c78d6e2bbdc1c6c742f955e3caf3bbe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 6, 2026.
Transparency logRelease files / rewloy-0.2.4-py3-none-any.whl
| Download URL | rewloy-0.2.4-py3-none-any.whl |
|---|---|
| Size | 182.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0e9ecf96add101633e6bef024096309579d784b1d78145ea7f78d1e0bddbaf04
|
|
BLAKE2b-256 checksum How to use checksums |
a5beb37afc51cd34296b7172c00c492cc2488e0f21af5f54e7992526986901f2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 6, 2026.
Transparency log