Guidance for AI coding agents (Claude Code, Codex, Cursor, Copilot, …) working on
or integrating the Heleket Python SDK. Humans: this doubles as a fast
architecture tour — see docs/ for the full reference.
The reference Python SDK for the Heleket cryptocurrency
payment API: payments, payouts, balance, exchange rates, services, and signed
webhooks. Typed end-to-end (Pydantic v2 models, ships py.typed).
- Package:
heleket-sdkon PyPI · import asheleket_sdk. - Python 3.11+ (uses
StrEnum,X | Ytypes,Self). - Runtime deps: Pydantic v2 (
>=2.5) and requests (>=2.31); everything else is stdlib. - Upstream API docs: https://doc.heleket.com.
| Path | Purpose |
|---|---|
src/heleket_sdk/ |
Package source. Public names re-exported from __init__.py; *.py prefixed _ are internal. |
src/heleket_sdk/py.typed |
PEP 561 marker — keep it; it's what makes the types ship. |
tests/ |
pytest suite + tests/fakes.py FakeTransport for offline HTTP. |
examples/ |
Twelve runnable scripts (01..12), one per endpoint + _bootstrap.py. Need .env. |
bin/heleket-webhook-inspect |
CLI to verify/dump a webhook payload (entry point heleket_sdk._cli:main). |
docs/ |
Full English reference (architecture, every endpoint, webhooks, …). |
Makefile |
make qa, make test, make lint, make typecheck, examples, Docker. |
.github/workflows/ |
ci.yml (gate) · publish.yml (Trusted Publishing on v* tags). |
HeleketPayment / HeleketPayout (one method per endpoint; typed in, typed out)
└─ BaseClient (the ONLY place that does HTTP: _post / _get → _send)
├─ ClientOptions (base_url, timeout, debug, logger, transport)
├─ sign() (md5(base64(body) + api_key); constant_time_equal)
├─ Transport (Protocol) → RequestsTransport (default, swappable)
└─ DebugSink (request/response trace when debug=True)
WebhookVerifier → WebhookPayload (verify inbound signatures)
PaymentStatus / PayoutStatus / AmlLinkStatus (is_final(), is_successful())
HeleketError ← ApiError / ValidationError / HttpError / SignatureError
types.py (Pydantic request/response models)
src/heleket_sdk/_base_client.py::_send() is the heart: it encodes params
(model_dump(by_alias=True, exclude_none=True)), signs the exact bytes, sets
the merchant / sign / Content-Type / User-Agent headers, dispatches via the
transport, and turns the response into either the parsed result or a typed error.
_post and _get both delegate to _send.
- Typed in, typed out. Endpoints take Pydantic request models (defined in
types.py) and return Pydantic response models. Response models useextra="ignore", so new server fields never break deserialization. - Sign the exact wire bytes.
_encode_bodyyields the JSON string andsign()hashes that same string. No params / empty dict →""→ sign collapses tomd5(api_key). Never re-encode between signing and sending. - Drop nulls. Optional fields default to
Noneand are stripped viaexclude_none=True(Pydantic) or a comprehension (raw dicts). - Keyword-only credentials.
HeleketPayment(merchant_id=..., api_key=..., options=...); positional args are rejected to prevent silently swapping the two strings. - Error model: HTTP 422 →
ValidationError(.fields= per-field messages);state != 0→ApiError(.http_status,.raw_body); transport failure →HttpError; bad webhook signature →SignatureError. All extendHeleketError. from __future__ import annotationsin every module; Python 3.11+ syntax; clients are thread-safe (immutable config + sharedrequests.Session).
- Add request/response models to
types.py(snake_case fields; alias only forfromand Heleket's camelCase pagination keys). - Add a method to
HeleketPayment/HeleketPayoutcallingself._post("/v1/...", request)(orself._get(path)for read-only, path-only endpoints; call with no args for an empty body). Guard "one of uuid/order_id" preconditions withValueError. ReturnModel.model_validate(result). - Add a unit test in
tests/test_payment.py/test_payout.pyusingFakeTransport— assert method, URL, signed headers, body, and parsed result. No network. - If user-facing, add a runnable
examples/NN_*.py(+ aMakefiletarget). - Run
make qa.
make install # poetry install --with dev
make test # pytest -q
make lint # ruff check .
make fmt # ruff format .
make typecheck # mypy (strict)
make qa # lint + format-check + typecheck + tests (the gate; CI runs 3.11 & 3.12)Tests are fully offline via FakeTransport — never hit the real API in tests.
Examples read credentials from .env (copy .env.example).
- Webhook byte-fidelity trap. Use
WebhookVerifier.verify_raw(raw_bytes)in production — it hashes the exact bytes Heleket sent (PHP-style\/slash escapes, insertion-order keys).verify(decoded_dict)re-encodes viajson.dumpsand is convenience-only — not safe for real traffic. Seedocs/06-webhooks.md. - Two separate API keys. Payments and payouts use different keys; a webhook of
the wrong kind fails verification.
HeleketPaymentvsHeleketPayout. - Refund is the cross-key exception.
/v1/payment/refundis a payment-domain path but is signed with the payout key, so it lives onHeleketPayout.refund().HeleketPaymentdoes not exposerefund()— calling it raisesAttributeError. get_exchange_ratesis the only GET (/v1/exchange-rate/{ccy}/list; input in the path, empty signed body). Every other endpoint is a POST, even reads.- AML links (
get_aml_links(uuid=None, order_id=None)) return questionnaire links for a blocked (locked) payment; statuses areAmlLinkStatus. - Always verify webhook signatures before trusting a payload; whitelist
Heleket's source IP
31.133.220.8. - Never log API keys. Debug entries carry method, URL, and body only — never
the
signheader or the key.
- One PyPI package = one repo: published from
github.com/Heleket/python-sdk. - Version lives in two places:
pyproject.tomlandsrc/heleket_sdk/_version.py(the latter feeds theUser-Agent). Bump both. - Tagging
vX.Y.Ztriggers.github/workflows/publish.yml, which builds,twine checks, asserts the tag matches thepyprojectversion, and publishes to PyPI via Trusted Publishing (OIDC — no API tokens). - See
CHANGELOG.mdandUPGRADING.md.