myntkaup 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ node_modules/
2
+ dist/
3
+ coverage/
4
+ *.tsbuildinfo
5
+ .env
6
+ .env.*
7
+ !.env.example
8
+ *.key
9
+ *.pem
10
+ .DS_Store
11
+ .vscode/
12
+ .idea/
13
+ npm-debug.log*
14
+ pnpm-debug.log*
15
+ __pycache__/
16
+ *.py[cod]
17
+ .pytest_cache/
18
+ .mypy_cache/
19
+ .ruff_cache/
20
+ *.egg-info/
21
+ .venv/
22
+ # pnpm pack:cli (docs/publishing.md)
23
+ .packs/
myntkaup-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Myntkaup ehf.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,310 @@
1
+ Metadata-Version: 2.5
2
+ Name: myntkaup
3
+ Version: 0.1.0
4
+ Summary: Python client for the Myntkaup public API: signed requests, retries, typed errors and pagination.
5
+ Project-URL: Homepage, https://developers.myntkaup.is
6
+ Project-URL: Source, https://github.com/myntkaup/myntkaup-public-api/tree/main/python
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: api,bitcoin,crypto,myntkaup,sdk
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: cryptography>=41
23
+ Requires-Dist: httpx<1,>=0.25
24
+ Provides-Extra: dev
25
+ Requires-Dist: anyio>=4; extra == 'dev'
26
+ Requires-Dist: mypy>=1.11; extra == 'dev'
27
+ Requires-Dist: pytest>=8; extra == 'dev'
28
+ Requires-Dist: ruff>=0.6; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # myntkaup
32
+
33
+ The Python client for the [Myntkaup public API](https://developers.myntkaup.is). It signs every request, retries what is safe to retry, sends idempotency keys, follows pagination cursors, and raises typed exceptions.
34
+
35
+ - Python 3.10 or later. Typed (`py.typed`).
36
+ - Two dependencies: `httpx` and `cryptography`.
37
+ - `Myntkaup` (synchronous) and `AsyncMyntkaup` (asyncio), with the same methods.
38
+
39
+ ## Install
40
+
41
+ ```sh
42
+ pip install myntkaup
43
+ ```
44
+
45
+ ## Configure
46
+
47
+ Create a key in the Myntkaup webapp under **Settings → API keys**. You get a key id (`mk_live_…`, or `mk_test_…` on staging) and a private key (`mk_sk_…`). Myntkaup stores only the public key, so keep the private key safe, e.g. in a secrets manager.
48
+
49
+ ```python
50
+ from myntkaup import Myntkaup
51
+
52
+ client = Myntkaup() # reads MYNTKAUP_API_KEY_ID and MYNTKAUP_API_PRIVATE_KEY
53
+ # or
54
+ client = Myntkaup(key_id="mk_live_…", private_key="mk_sk_…")
55
+ ```
56
+
57
+ | Option | Default |
58
+ | ------------- | ----------------------------------------------------------------------------------------------- |
59
+ | `key_id` | `MYNTKAUP_API_KEY_ID` |
60
+ | `private_key` | `MYNTKAUP_API_PRIVATE_KEY` |
61
+ | `base_url` | `https://public-api.myntkaup.is`; `https://public-api.staging.myntkaup.is` for a `mk_test_` key |
62
+ | `timeout` | `30.0` seconds, per attempt |
63
+ | `max_retries` | `2` |
64
+ | `transport` | an `httpx` transport, e.g. `httpx.MockTransport` in tests |
65
+
66
+ The client holds a connection pool: use it as a context manager (`with Myntkaup() as client:`) or call `client.close()` when done.
67
+
68
+ Responses are plain dicts, exactly as the API sends them, with snake_case keys, so the JSON in the API reference applies as is. They are typed as `TypedDict`s (`myntkaup.Quote`, `myntkaup.Trade`, …). Amounts are decimal strings (`"0.00104210"`); pass amounts as strings or `decimal.Decimal`, never as floats. Use `Decimal(value)` to calculate with them.
69
+
70
+ ## Examples
71
+
72
+ ### Status, account and market data
73
+
74
+ ```python
75
+ status = client.status() # unsigned; status["server_time"] is the server clock
76
+ me = client.me() # the account and this key
77
+ print(me["account"]["tier"], me["key"]["scopes"])
78
+
79
+ assets = client.assets.list()["data"]
80
+ prices = client.prices.list() # every asset at your tier, in USDC
81
+ btc = client.prices.get("BTC") # {"asset", "quote_currency", "buy", "sell", "updated_at"}
82
+ ```
83
+
84
+ ### Balances and limits
85
+
86
+ ```python
87
+ balances = client.balances.list()
88
+ for balance in balances["data"]:
89
+ print(balance["asset"], balance["available"], balance["held"])
90
+
91
+ limits = client.limits.get()
92
+ print(limits["trade"]["daily_volume_used_usdc"], limits["rate_limits"]["classes"]["quote"])
93
+ ```
94
+
95
+ ### Trading
96
+
97
+ The quick way: `buy` and `sell` create a quote and accept it.
98
+
99
+ ```python
100
+ trade = client.buy(asset="BTC", quote_amount="100.00") # spend 100 USDC
101
+ sold = client.sell(asset="BTC", base_amount="0.001") # sell 0.001 BTC
102
+
103
+ if trade["status"] == "filled":
104
+ print(f"Bought {trade['base_amount']} BTC at {trade['price']}")
105
+ ```
106
+
107
+ The helpers return the trade whatever its `status`, so check it: a trade can be `failed` (see `failure_reason`). A trade still `pending` (the API answered 202) is polled with `trades.get` for up to 30 seconds. If it is still pending after that, or the key can't read trades (`trades.get` needs the `transactions:read` scope), you get the pending trade back: look it up later with `client.trades.get(trade["id"])`.
108
+
109
+ Step by step:
110
+
111
+ ```python
112
+ quote = client.quotes.create(asset="BTC", side="buy", quote_amount="100.00")
113
+ # or asset="BTC", side="sell", base_amount="0.001": exactly one of the two
114
+ print(quote["price"], quote["base_amount"], quote["expires_at"]) # valid for about 10 seconds
115
+
116
+ trade = client.trades.create(quote_id=quote["id"]) # sends an Idempotency-Key
117
+ quote = client.quotes.get(quote["id"]) # status: open, accepted, expired, filled or failed
118
+ ```
119
+
120
+ `trades.create` sends a random `Idempotency-Key` (UUID v4) unless you give one, and reuses it on its retries, so a retried request never trades twice. Give your own to make retries across restarts safe:
121
+
122
+ ```python
123
+ client.trades.create(quote_id=quote["id"], idempotency_key=f"order-{order_id}")
124
+ ```
125
+
126
+ ### History
127
+
128
+ ```python
129
+ page = client.trades.list(asset="BTC", side="buy", limit=100)
130
+ print(page["data"], page["next_cursor"]) # pass next_cursor as cursor for the next page
131
+
132
+ trade = client.trades.get("tr_01JB8X2Q4M7Z3V9K5T1N6R0P2W")
133
+
134
+ # list_all follows next_cursor for you, one page at a time.
135
+ from datetime import datetime, timezone
136
+
137
+ for entry in client.transactions.list_all(
138
+ from_="2026-01-01T00:00:00Z", # from_ is sent as from
139
+ to=datetime.now(timezone.utc), # aware datetimes are sent as ISO 8601 UTC
140
+ type="trade",
141
+ lang="is", # labels in Icelandic; en is the default
142
+ ):
143
+ print(entry["created_at"], entry["label"], entry["changes"])
144
+
145
+ entry = client.transactions.get("tx_01JB8X2Q4M7Z3V9K5T1N6R0P2W", lang="en")
146
+ ```
147
+
148
+ `from` is a Python keyword, so the filter is `from_`. `from_` and `to` may be at most 366 days apart. `limit` is 1 to 100 (default 50).
149
+
150
+ ### Gift cards
151
+
152
+ ```python
153
+ card = client.giftcards.create(asset="BTC", amount="0.0005", recipient_name="Anna")
154
+ print(card["code"]) # treat it like cash
155
+
156
+ pdf = client.giftcards.pdf(card["id"]) # bytes
157
+ with open(f"{card['id']}.pdf", "wb") as file:
158
+ file.write(pdf)
159
+
160
+ page = client.giftcards.list(status="unredeemed")
161
+ one = client.giftcards.get(card["id"])
162
+ for giftcard in client.giftcards.list_all():
163
+ print(giftcard["id"], giftcard["status"])
164
+ ```
165
+
166
+ Creating gift cards needs the `giftcards:create` scope. Like trades, `giftcards.create` sends an `Idempotency-Key` unless you give one.
167
+
168
+ ### Withdrawals
169
+
170
+ To a bank account: one you have withdrawn to before, or a new Icelandic account in your own name. Bank withdrawals are always from your USDC balance; Myntkaup pays out in krónur, or in US dollars to an Arion foreign-currency account (03xx-38-…).
171
+
172
+ ```python
173
+ accounts = client.bank_accounts.list()["data"] # most recently used first
174
+ payout = client.withdrawals.create_bank(
175
+ amount="250.00", # USDC, taken from the balance, fee included
176
+ bank_account=accounts[0]["account_number"], # or a new one, e.g. "0133-26-012345"
177
+ )
178
+ print(payout["status"]) # pending: paid out on a business day
179
+ ```
180
+
181
+ To a crypto address. The account's email gets a confirmation link, and nothing is sent, or held, until it is opened (within 30 minutes):
182
+
183
+ ```python
184
+ withdrawal = client.withdrawals.create_crypto(
185
+ asset="BTC",
186
+ amount="0.01", # fee included
187
+ address="bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
188
+ travel_rule={"address_owner": "self", "wallet_type": "hosted", "vasp": "kraken"}, # your Kraken account
189
+ )
190
+ print(withdrawal["status"], withdrawal["confirm_by"]) # awaiting_confirmation, until when the link works
191
+
192
+ # After the link is opened: pending, then completed with tx_id. Unconfirmed: expired.
193
+ latest = client.withdrawals.get(withdrawal["id"])
194
+ ```
195
+
196
+ `travel_rule` says who receives the funds. To someone else's wallet, name them:
197
+
198
+ ```python
199
+ travel_rule = {
200
+ "address_owner": "other",
201
+ "wallet_type": "self_hosted", # or "hosted", with vasp (vasp_name when vasp is "other")
202
+ "beneficiary": {"type": "individual", "full_name": "Anna Þórsdóttir"},
203
+ # a company: {"type": "company", "company_name": "Dæmi ehf.", "registration_number": "5501012340"}
204
+ }
205
+ ```
206
+
207
+ Give `network` for an asset on several networks (USDC: `ethereum`, the default, or `solana`), and `memo` for XRP (destination tag) and HBAR.
208
+
209
+ ```python
210
+ page = client.withdrawals.list(type="crypto", status="pending")
211
+ for entry in client.withdrawals.list_all(from_="2026-01-01T00:00:00Z"):
212
+ print(entry["id"], entry["type"], entry["status"], entry["amount"])
213
+ ```
214
+
215
+ Withdrawing needs the `withdrawals:create` scope, which can also list bank accounts and read withdrawals (as `account:read` and `transactions:read` can). Both create methods send an `Idempotency-Key` unless you give one. When withdrawals are switched off for the API (`withdrawals_enabled` in `client.status()`), they fail with `forbidden`; an asset that can't be withdrawn on that network fails with `asset_not_withdrawable`.
216
+
217
+ ### Async
218
+
219
+ `AsyncMyntkaup` has the same methods as coroutines; `list_all` returns an async iterator.
220
+
221
+ ```python
222
+ import asyncio
223
+
224
+ from myntkaup import AsyncMyntkaup
225
+
226
+
227
+ async def main() -> None:
228
+ async with AsyncMyntkaup() as client:
229
+ balances, prices = await asyncio.gather(client.balances.list(), client.prices.list())
230
+ trade = await client.buy(asset="BTC", quote_amount="25.00")
231
+ async for entry in client.transactions.list_all(type="trade"):
232
+ print(entry["label"])
233
+
234
+
235
+ asyncio.run(main())
236
+ ```
237
+
238
+ ## Errors
239
+
240
+ Every non-2xx answer raises an `ApiError` subclass. Match on `code`, never on `message`.
241
+
242
+ | Exception | Status | Examples of `code` |
243
+ | --------------------- | ------- | ----------------------------------------------------------------------- |
244
+ | `ValidationError` | 400/422 | `invalid_request`, `insufficient_funds`, `quote_expired` |
245
+ | `AuthenticationError` | 401 | `invalid_signature`, `timestamp_out_of_range`, `key_revoked` |
246
+ | `PermissionError` | 403 | `insufficient_scope`, `ip_not_allowed`, `account_restricted` |
247
+ | `NotFoundError` | 404 | `not_found` |
248
+ | `ConflictError` | 409 | `idempotency_conflict`, `idempotency_in_progress`, `quote_already_used` |
249
+ | `RateLimitError` | 429 | `rate_limited`, `too_many_concurrent_requests`, `key_suspended` |
250
+ | `ServiceError` | 5xx | `trading_halted`, `maintenance`, `service_unavailable` |
251
+ | `ApiError` | other | `method_not_allowed`, `payload_too_large` |
252
+
253
+ ```python
254
+ import myntkaup
255
+
256
+ try:
257
+ client.buy(asset="BTC", quote_amount="100.00")
258
+ except myntkaup.RateLimitError as error:
259
+ print(f"Rate limited; retry in {error.retry_after} s", error.rate_limit)
260
+ except myntkaup.ApiError as error:
261
+ print(error.status, error.code, error.message, error.request_id)
262
+ except myntkaup.ConnectionError as error:
263
+ print("No answer:", error, error.__cause__)
264
+ ```
265
+
266
+ An `ApiError` has `status`, `code`, `message`, `request_id` (quote it to support), `retry_after` (seconds, from `Retry-After`), `rate_limit` and `rate_limit_policy` (the raw `RateLimit` and `RateLimit-Policy` headers) and `headers`. A body without the error envelope, such as a load balancer's error page, has the code `unknown_error`.
267
+
268
+ A network failure or a timeout raises `myntkaup.ConnectionError`, which is not an `ApiError`. Both extend `MyntkaupError`. `myntkaup.PermissionError` and `myntkaup.ConnectionError` are not the built-in exceptions of the same names, so refer to them through the module (or import them under other names) rather than with `from myntkaup import *`. Invalid arguments, such as a float for an amount, raise `TypeError` or `ValueError` before anything is sent.
269
+
270
+ ## Retries
271
+
272
+ Failed requests are retried at most `max_retries` times (2 by default), with exponential backoff and jitter (0.25-0.5 s, then 0.5-1 s, …, at most 8 s). When the server sends `Retry-After`, the client waits at least that long.
273
+
274
+ - GET requests are retried on 429, 502, 503 and 504, and on network errors and timeouts.
275
+ - POST requests are retried on the same, but only when they carry an `Idempotency-Key`: always for `trades.create`, `giftcards.create`, `withdrawals.create_bank` and `withdrawals.create_crypto`, and for `quotes.create` only if you pass `idempotency_key`.
276
+ - Never retried: 400, 401, 403, 404, 409, 422 and 500, and 429 `key_suspended`.
277
+ - A `Retry-After` of more than 60 seconds isn't waited for: the error is raised, with `retry_after` set.
278
+
279
+ Each attempt is signed again, with a new timestamp and nonce. Set `max_retries=0` to handle retries yourself.
280
+
281
+ Rate limits are per key and per account (see `client.limits.get()`), with at most 5 requests in flight per key. Don't fan out hundreds of concurrent requests; a key that is rate limited most of the time for 10 minutes is suspended for 15.
282
+
283
+ ## Signing requests yourself
284
+
285
+ To call the API with another HTTP client, `sign_request` returns the four `MK-*` headers for one request:
286
+
287
+ ```python
288
+ import json
289
+
290
+ import httpx
291
+ from myntkaup import sign_request
292
+
293
+ body = json.dumps({"asset": "BTC", "side": "buy", "quote_amount": "100.00"}).encode()
294
+ headers = sign_request(
295
+ key_id="mk_live_…",
296
+ private_key="mk_sk_…",
297
+ method="POST",
298
+ path_and_query="/v1/quotes", # exactly as on the request line, query string included
299
+ body=body, # the exact bytes you send
300
+ )
301
+ httpx.post(
302
+ "https://public-api.myntkaup.is/v1/quotes",
303
+ content=body,
304
+ headers={**headers, "Content-Type": "application/json"},
305
+ )
306
+ ```
307
+
308
+ Sign every request afresh: a nonce must never repeat, and the timestamp must be within 30 seconds of the server clock (`server_time` in `client.status()`).
309
+
310
+ New response fields may be added within v1, so don't fail on keys you don't know.
@@ -0,0 +1,280 @@
1
+ # myntkaup
2
+
3
+ The Python client for the [Myntkaup public API](https://developers.myntkaup.is). It signs every request, retries what is safe to retry, sends idempotency keys, follows pagination cursors, and raises typed exceptions.
4
+
5
+ - Python 3.10 or later. Typed (`py.typed`).
6
+ - Two dependencies: `httpx` and `cryptography`.
7
+ - `Myntkaup` (synchronous) and `AsyncMyntkaup` (asyncio), with the same methods.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ pip install myntkaup
13
+ ```
14
+
15
+ ## Configure
16
+
17
+ Create a key in the Myntkaup webapp under **Settings → API keys**. You get a key id (`mk_live_…`, or `mk_test_…` on staging) and a private key (`mk_sk_…`). Myntkaup stores only the public key, so keep the private key safe, e.g. in a secrets manager.
18
+
19
+ ```python
20
+ from myntkaup import Myntkaup
21
+
22
+ client = Myntkaup() # reads MYNTKAUP_API_KEY_ID and MYNTKAUP_API_PRIVATE_KEY
23
+ # or
24
+ client = Myntkaup(key_id="mk_live_…", private_key="mk_sk_…")
25
+ ```
26
+
27
+ | Option | Default |
28
+ | ------------- | ----------------------------------------------------------------------------------------------- |
29
+ | `key_id` | `MYNTKAUP_API_KEY_ID` |
30
+ | `private_key` | `MYNTKAUP_API_PRIVATE_KEY` |
31
+ | `base_url` | `https://public-api.myntkaup.is`; `https://public-api.staging.myntkaup.is` for a `mk_test_` key |
32
+ | `timeout` | `30.0` seconds, per attempt |
33
+ | `max_retries` | `2` |
34
+ | `transport` | an `httpx` transport, e.g. `httpx.MockTransport` in tests |
35
+
36
+ The client holds a connection pool: use it as a context manager (`with Myntkaup() as client:`) or call `client.close()` when done.
37
+
38
+ Responses are plain dicts, exactly as the API sends them, with snake_case keys, so the JSON in the API reference applies as is. They are typed as `TypedDict`s (`myntkaup.Quote`, `myntkaup.Trade`, …). Amounts are decimal strings (`"0.00104210"`); pass amounts as strings or `decimal.Decimal`, never as floats. Use `Decimal(value)` to calculate with them.
39
+
40
+ ## Examples
41
+
42
+ ### Status, account and market data
43
+
44
+ ```python
45
+ status = client.status() # unsigned; status["server_time"] is the server clock
46
+ me = client.me() # the account and this key
47
+ print(me["account"]["tier"], me["key"]["scopes"])
48
+
49
+ assets = client.assets.list()["data"]
50
+ prices = client.prices.list() # every asset at your tier, in USDC
51
+ btc = client.prices.get("BTC") # {"asset", "quote_currency", "buy", "sell", "updated_at"}
52
+ ```
53
+
54
+ ### Balances and limits
55
+
56
+ ```python
57
+ balances = client.balances.list()
58
+ for balance in balances["data"]:
59
+ print(balance["asset"], balance["available"], balance["held"])
60
+
61
+ limits = client.limits.get()
62
+ print(limits["trade"]["daily_volume_used_usdc"], limits["rate_limits"]["classes"]["quote"])
63
+ ```
64
+
65
+ ### Trading
66
+
67
+ The quick way: `buy` and `sell` create a quote and accept it.
68
+
69
+ ```python
70
+ trade = client.buy(asset="BTC", quote_amount="100.00") # spend 100 USDC
71
+ sold = client.sell(asset="BTC", base_amount="0.001") # sell 0.001 BTC
72
+
73
+ if trade["status"] == "filled":
74
+ print(f"Bought {trade['base_amount']} BTC at {trade['price']}")
75
+ ```
76
+
77
+ The helpers return the trade whatever its `status`, so check it: a trade can be `failed` (see `failure_reason`). A trade still `pending` (the API answered 202) is polled with `trades.get` for up to 30 seconds. If it is still pending after that, or the key can't read trades (`trades.get` needs the `transactions:read` scope), you get the pending trade back: look it up later with `client.trades.get(trade["id"])`.
78
+
79
+ Step by step:
80
+
81
+ ```python
82
+ quote = client.quotes.create(asset="BTC", side="buy", quote_amount="100.00")
83
+ # or asset="BTC", side="sell", base_amount="0.001": exactly one of the two
84
+ print(quote["price"], quote["base_amount"], quote["expires_at"]) # valid for about 10 seconds
85
+
86
+ trade = client.trades.create(quote_id=quote["id"]) # sends an Idempotency-Key
87
+ quote = client.quotes.get(quote["id"]) # status: open, accepted, expired, filled or failed
88
+ ```
89
+
90
+ `trades.create` sends a random `Idempotency-Key` (UUID v4) unless you give one, and reuses it on its retries, so a retried request never trades twice. Give your own to make retries across restarts safe:
91
+
92
+ ```python
93
+ client.trades.create(quote_id=quote["id"], idempotency_key=f"order-{order_id}")
94
+ ```
95
+
96
+ ### History
97
+
98
+ ```python
99
+ page = client.trades.list(asset="BTC", side="buy", limit=100)
100
+ print(page["data"], page["next_cursor"]) # pass next_cursor as cursor for the next page
101
+
102
+ trade = client.trades.get("tr_01JB8X2Q4M7Z3V9K5T1N6R0P2W")
103
+
104
+ # list_all follows next_cursor for you, one page at a time.
105
+ from datetime import datetime, timezone
106
+
107
+ for entry in client.transactions.list_all(
108
+ from_="2026-01-01T00:00:00Z", # from_ is sent as from
109
+ to=datetime.now(timezone.utc), # aware datetimes are sent as ISO 8601 UTC
110
+ type="trade",
111
+ lang="is", # labels in Icelandic; en is the default
112
+ ):
113
+ print(entry["created_at"], entry["label"], entry["changes"])
114
+
115
+ entry = client.transactions.get("tx_01JB8X2Q4M7Z3V9K5T1N6R0P2W", lang="en")
116
+ ```
117
+
118
+ `from` is a Python keyword, so the filter is `from_`. `from_` and `to` may be at most 366 days apart. `limit` is 1 to 100 (default 50).
119
+
120
+ ### Gift cards
121
+
122
+ ```python
123
+ card = client.giftcards.create(asset="BTC", amount="0.0005", recipient_name="Anna")
124
+ print(card["code"]) # treat it like cash
125
+
126
+ pdf = client.giftcards.pdf(card["id"]) # bytes
127
+ with open(f"{card['id']}.pdf", "wb") as file:
128
+ file.write(pdf)
129
+
130
+ page = client.giftcards.list(status="unredeemed")
131
+ one = client.giftcards.get(card["id"])
132
+ for giftcard in client.giftcards.list_all():
133
+ print(giftcard["id"], giftcard["status"])
134
+ ```
135
+
136
+ Creating gift cards needs the `giftcards:create` scope. Like trades, `giftcards.create` sends an `Idempotency-Key` unless you give one.
137
+
138
+ ### Withdrawals
139
+
140
+ To a bank account: one you have withdrawn to before, or a new Icelandic account in your own name. Bank withdrawals are always from your USDC balance; Myntkaup pays out in krónur, or in US dollars to an Arion foreign-currency account (03xx-38-…).
141
+
142
+ ```python
143
+ accounts = client.bank_accounts.list()["data"] # most recently used first
144
+ payout = client.withdrawals.create_bank(
145
+ amount="250.00", # USDC, taken from the balance, fee included
146
+ bank_account=accounts[0]["account_number"], # or a new one, e.g. "0133-26-012345"
147
+ )
148
+ print(payout["status"]) # pending: paid out on a business day
149
+ ```
150
+
151
+ To a crypto address. The account's email gets a confirmation link, and nothing is sent, or held, until it is opened (within 30 minutes):
152
+
153
+ ```python
154
+ withdrawal = client.withdrawals.create_crypto(
155
+ asset="BTC",
156
+ amount="0.01", # fee included
157
+ address="bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
158
+ travel_rule={"address_owner": "self", "wallet_type": "hosted", "vasp": "kraken"}, # your Kraken account
159
+ )
160
+ print(withdrawal["status"], withdrawal["confirm_by"]) # awaiting_confirmation, until when the link works
161
+
162
+ # After the link is opened: pending, then completed with tx_id. Unconfirmed: expired.
163
+ latest = client.withdrawals.get(withdrawal["id"])
164
+ ```
165
+
166
+ `travel_rule` says who receives the funds. To someone else's wallet, name them:
167
+
168
+ ```python
169
+ travel_rule = {
170
+ "address_owner": "other",
171
+ "wallet_type": "self_hosted", # or "hosted", with vasp (vasp_name when vasp is "other")
172
+ "beneficiary": {"type": "individual", "full_name": "Anna Þórsdóttir"},
173
+ # a company: {"type": "company", "company_name": "Dæmi ehf.", "registration_number": "5501012340"}
174
+ }
175
+ ```
176
+
177
+ Give `network` for an asset on several networks (USDC: `ethereum`, the default, or `solana`), and `memo` for XRP (destination tag) and HBAR.
178
+
179
+ ```python
180
+ page = client.withdrawals.list(type="crypto", status="pending")
181
+ for entry in client.withdrawals.list_all(from_="2026-01-01T00:00:00Z"):
182
+ print(entry["id"], entry["type"], entry["status"], entry["amount"])
183
+ ```
184
+
185
+ Withdrawing needs the `withdrawals:create` scope, which can also list bank accounts and read withdrawals (as `account:read` and `transactions:read` can). Both create methods send an `Idempotency-Key` unless you give one. When withdrawals are switched off for the API (`withdrawals_enabled` in `client.status()`), they fail with `forbidden`; an asset that can't be withdrawn on that network fails with `asset_not_withdrawable`.
186
+
187
+ ### Async
188
+
189
+ `AsyncMyntkaup` has the same methods as coroutines; `list_all` returns an async iterator.
190
+
191
+ ```python
192
+ import asyncio
193
+
194
+ from myntkaup import AsyncMyntkaup
195
+
196
+
197
+ async def main() -> None:
198
+ async with AsyncMyntkaup() as client:
199
+ balances, prices = await asyncio.gather(client.balances.list(), client.prices.list())
200
+ trade = await client.buy(asset="BTC", quote_amount="25.00")
201
+ async for entry in client.transactions.list_all(type="trade"):
202
+ print(entry["label"])
203
+
204
+
205
+ asyncio.run(main())
206
+ ```
207
+
208
+ ## Errors
209
+
210
+ Every non-2xx answer raises an `ApiError` subclass. Match on `code`, never on `message`.
211
+
212
+ | Exception | Status | Examples of `code` |
213
+ | --------------------- | ------- | ----------------------------------------------------------------------- |
214
+ | `ValidationError` | 400/422 | `invalid_request`, `insufficient_funds`, `quote_expired` |
215
+ | `AuthenticationError` | 401 | `invalid_signature`, `timestamp_out_of_range`, `key_revoked` |
216
+ | `PermissionError` | 403 | `insufficient_scope`, `ip_not_allowed`, `account_restricted` |
217
+ | `NotFoundError` | 404 | `not_found` |
218
+ | `ConflictError` | 409 | `idempotency_conflict`, `idempotency_in_progress`, `quote_already_used` |
219
+ | `RateLimitError` | 429 | `rate_limited`, `too_many_concurrent_requests`, `key_suspended` |
220
+ | `ServiceError` | 5xx | `trading_halted`, `maintenance`, `service_unavailable` |
221
+ | `ApiError` | other | `method_not_allowed`, `payload_too_large` |
222
+
223
+ ```python
224
+ import myntkaup
225
+
226
+ try:
227
+ client.buy(asset="BTC", quote_amount="100.00")
228
+ except myntkaup.RateLimitError as error:
229
+ print(f"Rate limited; retry in {error.retry_after} s", error.rate_limit)
230
+ except myntkaup.ApiError as error:
231
+ print(error.status, error.code, error.message, error.request_id)
232
+ except myntkaup.ConnectionError as error:
233
+ print("No answer:", error, error.__cause__)
234
+ ```
235
+
236
+ An `ApiError` has `status`, `code`, `message`, `request_id` (quote it to support), `retry_after` (seconds, from `Retry-After`), `rate_limit` and `rate_limit_policy` (the raw `RateLimit` and `RateLimit-Policy` headers) and `headers`. A body without the error envelope, such as a load balancer's error page, has the code `unknown_error`.
237
+
238
+ A network failure or a timeout raises `myntkaup.ConnectionError`, which is not an `ApiError`. Both extend `MyntkaupError`. `myntkaup.PermissionError` and `myntkaup.ConnectionError` are not the built-in exceptions of the same names, so refer to them through the module (or import them under other names) rather than with `from myntkaup import *`. Invalid arguments, such as a float for an amount, raise `TypeError` or `ValueError` before anything is sent.
239
+
240
+ ## Retries
241
+
242
+ Failed requests are retried at most `max_retries` times (2 by default), with exponential backoff and jitter (0.25-0.5 s, then 0.5-1 s, …, at most 8 s). When the server sends `Retry-After`, the client waits at least that long.
243
+
244
+ - GET requests are retried on 429, 502, 503 and 504, and on network errors and timeouts.
245
+ - POST requests are retried on the same, but only when they carry an `Idempotency-Key`: always for `trades.create`, `giftcards.create`, `withdrawals.create_bank` and `withdrawals.create_crypto`, and for `quotes.create` only if you pass `idempotency_key`.
246
+ - Never retried: 400, 401, 403, 404, 409, 422 and 500, and 429 `key_suspended`.
247
+ - A `Retry-After` of more than 60 seconds isn't waited for: the error is raised, with `retry_after` set.
248
+
249
+ Each attempt is signed again, with a new timestamp and nonce. Set `max_retries=0` to handle retries yourself.
250
+
251
+ Rate limits are per key and per account (see `client.limits.get()`), with at most 5 requests in flight per key. Don't fan out hundreds of concurrent requests; a key that is rate limited most of the time for 10 minutes is suspended for 15.
252
+
253
+ ## Signing requests yourself
254
+
255
+ To call the API with another HTTP client, `sign_request` returns the four `MK-*` headers for one request:
256
+
257
+ ```python
258
+ import json
259
+
260
+ import httpx
261
+ from myntkaup import sign_request
262
+
263
+ body = json.dumps({"asset": "BTC", "side": "buy", "quote_amount": "100.00"}).encode()
264
+ headers = sign_request(
265
+ key_id="mk_live_…",
266
+ private_key="mk_sk_…",
267
+ method="POST",
268
+ path_and_query="/v1/quotes", # exactly as on the request line, query string included
269
+ body=body, # the exact bytes you send
270
+ )
271
+ httpx.post(
272
+ "https://public-api.myntkaup.is/v1/quotes",
273
+ content=body,
274
+ headers={**headers, "Content-Type": "application/json"},
275
+ )
276
+ ```
277
+
278
+ Sign every request afresh: a nonce must never repeat, and the timestamp must be within 30 seconds of the server clock (`server_time` in `client.status()`).
279
+
280
+ New response fields may be added within v1, so don't fail on keys you don't know.