myntkaup
The Python client for the Myntkaup public API. It signs every request, retries what is safe to retry, sends idempotency keys, follows pagination cursors, and raises typed exceptions.
- Python 3.10 or later. Typed (
py.typed). - Two dependencies:
httpxandcryptography. Myntkaup(synchronous) andAsyncMyntkaup(asyncio), with the same methods.
Install
pip install myntkaup
Configure
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.
from myntkaup import Myntkaup
client = Myntkaup() # reads MYNTKAUP_API_KEY_ID and MYNTKAUP_API_PRIVATE_KEY
# or
client = Myntkaup(key_id="mk_live_…", private_key="mk_sk_…")
| Option | Default |
|---|---|
key_id |
MYNTKAUP_API_KEY_ID |
private_key |
MYNTKAUP_API_PRIVATE_KEY |
base_url |
https://public-api.myntkaup.is; https://public-api.staging.myntkaup.is for a mk_test_ key |
timeout |
30.0 seconds, per attempt |
max_retries |
2 |
transport |
an httpx transport, e.g. httpx.MockTransport in tests |
The client holds a connection pool: use it as a context manager (with Myntkaup() as client:) or call client.close() when done.
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 TypedDicts (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.
Examples
Status, account and market data
status = client.status() # unsigned; status["server_time"] is the server clock
me = client.me() # the account and this key
print(me["account"]["tier"], me["key"]["scopes"])
assets = client.assets.list()["data"]
prices = client.prices.list() # every asset at your tier, in USDC
btc = client.prices.get("BTC") # {"asset", "quote_currency", "buy", "sell", "updated_at"}
Balances and limits
balances = client.balances.list()
for balance in balances["data"]:
print(balance["asset"], balance["available"], balance["held"])
limits = client.limits.get()
print(limits["trade"]["daily_volume_used_usdc"], limits["rate_limits"]["classes"]["quote"])
Trading
The quick way: buy and sell create a quote and accept it.
trade = client.buy(asset="BTC", quote_amount="100.00") # spend 100 USDC
sold = client.sell(asset="BTC", base_amount="0.001") # sell 0.001 BTC
if trade["status"] == "filled":
print(f"Bought {trade['base_amount']} BTC at {trade['price']}")
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"]).
Step by step:
quote = client.quotes.create(asset="BTC", side="buy", quote_amount="100.00")
# or asset="BTC", side="sell", base_amount="0.001": exactly one of the two
print(quote["price"], quote["base_amount"], quote["expires_at"]) # valid for about 10 seconds
trade = client.trades.create(quote_id=quote["id"]) # sends an Idempotency-Key
quote = client.quotes.get(quote["id"]) # status: open, accepted, expired, filled or failed
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:
client.trades.create(quote_id=quote["id"], idempotency_key=f"order-{order_id}")
History
page = client.trades.list(asset="BTC", side="buy", limit=100)
print(page["data"], page["next_cursor"]) # pass next_cursor as cursor for the next page
trade = client.trades.get("tr_01JB8X2Q4M7Z3V9K5T1N6R0P2W")
# list_all follows next_cursor for you, one page at a time.
from datetime import datetime, timezone
for entry in client.transactions.list_all(
from_="2026-01-01T00:00:00Z", # from_ is sent as from
to=datetime.now(timezone.utc), # aware datetimes are sent as ISO 8601 UTC
type="trade",
lang="is", # labels in Icelandic; en is the default
):
print(entry["created_at"], entry["label"], entry["changes"])
entry = client.transactions.get("tx_01JB8X2Q4M7Z3V9K5T1N6R0P2W", lang="en")
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).
Gift cards
card = client.giftcards.create(asset="BTC", amount="0.0005", recipient_name="Anna")
print(card["code"]) # treat it like cash
pdf = client.giftcards.pdf(card["id"]) # bytes
with open(f"{card['id']}.pdf", "wb") as file:
file.write(pdf)
page = client.giftcards.list(status="unredeemed")
one = client.giftcards.get(card["id"])
for giftcard in client.giftcards.list_all():
print(giftcard["id"], giftcard["status"])
Creating gift cards needs the giftcards:create scope. Like trades, giftcards.create sends an Idempotency-Key unless you give one.
Withdrawals
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-…).
accounts = client.bank_accounts.list()["data"] # most recently used first
payout = client.withdrawals.create_bank(
amount="250.00", # USDC, taken from the balance, fee included
bank_account=accounts[0]["account_number"], # or a new one, e.g. "0133-26-012345"
)
print(payout["status"]) # pending: paid out on a business day
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):
withdrawal = client.withdrawals.create_crypto(
asset="BTC",
amount="0.01", # fee included
address="bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
travel_rule={"address_owner": "self", "wallet_type": "hosted", "vasp": "kraken"}, # your Kraken account
)
print(withdrawal["status"], withdrawal["confirm_by"]) # awaiting_confirmation, until when the link works
# After the link is opened: pending, then completed with tx_id. Unconfirmed: expired.
latest = client.withdrawals.get(withdrawal["id"])
travel_rule says who receives the funds. To someone else's wallet, name them:
travel_rule = {
"address_owner": "other",
"wallet_type": "self_hosted", # or "hosted", with vasp (vasp_name when vasp is "other")
"beneficiary": {"type": "individual", "full_name": "Anna Þórsdóttir"},
# a company: {"type": "company", "company_name": "Dæmi ehf.", "registration_number": "5501012340"}
}
Give network for an asset on several networks (USDC: ethereum, the default, or solana), and memo for XRP (destination tag) and HBAR.
page = client.withdrawals.list(type="crypto", status="pending")
for entry in client.withdrawals.list_all(from_="2026-01-01T00:00:00Z"):
print(entry["id"], entry["type"], entry["status"], entry["amount"])
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.
Async
AsyncMyntkaup has the same methods as coroutines; list_all returns an async iterator.
import asyncio
from myntkaup import AsyncMyntkaup
async def main() -> None:
async with AsyncMyntkaup() as client:
balances, prices = await asyncio.gather(client.balances.list(), client.prices.list())
trade = await client.buy(asset="BTC", quote_amount="25.00")
async for entry in client.transactions.list_all(type="trade"):
print(entry["label"])
asyncio.run(main())
Errors
Every non-2xx answer raises an ApiError subclass. Match on code, never on message.
| Exception | Status | Examples of code |
|---|---|---|
ValidationError |
400/422 | invalid_request, insufficient_funds, quote_expired |
AuthenticationError |
401 | invalid_signature, timestamp_out_of_range, key_revoked |
PermissionError |
403 | insufficient_scope, ip_not_allowed, account_restricted |
NotFoundError |
404 | not_found |
ConflictError |
409 | idempotency_conflict, idempotency_in_progress, quote_already_used |
RateLimitError |
429 | rate_limited, too_many_concurrent_requests, key_suspended |
ServiceError |
5xx | trading_halted, maintenance, service_unavailable |
ApiError |
other | method_not_allowed, payload_too_large |
import myntkaup
try:
client.buy(asset="BTC", quote_amount="100.00")
except myntkaup.RateLimitError as error:
print(f"Rate limited; retry in {error.retry_after} s", error.rate_limit)
except myntkaup.ApiError as error:
print(error.status, error.code, error.message, error.request_id)
except myntkaup.ConnectionError as error:
print("No answer:", error, error.__cause__)
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.
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.
Retries
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.
- GET requests are retried on 429, 502, 503 and 504, and on network errors and timeouts.
- POST requests are retried on the same, but only when they carry an
Idempotency-Key: always fortrades.create,giftcards.create,withdrawals.create_bankandwithdrawals.create_crypto, and forquotes.createonly if you passidempotency_key. - Never retried: 400, 401, 403, 404, 409, 422 and 500, and 429
key_suspended. - A
Retry-Afterof more than 60 seconds isn't waited for: the error is raised, withretry_afterset.
Each attempt is signed again, with a new timestamp and nonce. Set max_retries=0 to handle retries yourself.
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.
Signing requests yourself
To call the API with another HTTP client, sign_request returns the four MK-* headers for one request:
import json
import httpx
from myntkaup import sign_request
body = json.dumps({"asset": "BTC", "side": "buy", "quote_amount": "100.00"}).encode()
headers = sign_request(
key_id="mk_live_…",
private_key="mk_sk_…",
method="POST",
path_and_query="/v1/quotes", # exactly as on the request line, query string included
body=body, # the exact bytes you send
)
httpx.post(
"https://public-api.myntkaup.is/v1/quotes",
content=body,
headers={**headers, "Content-Type": "application/json"},
)
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()).
New response fields may be added within v1, so don't fail on keys you don't know.
Metadata
Release files for myntkaup 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| myntkaup-0.1.0.tar.gz | 36.6 kB | Without provenance | Details for myntkaup-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| myntkaup-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for myntkaup-0.1.0-py3-none-any.whl |
Total release size: 64.8 kB
Release files / myntkaup-0.1.0.tar.gz
| Download URL | myntkaup-0.1.0.tar.gz |
|---|---|
| Size | 36.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0d4d94828709fadad36779fceeaf7381a1bca96e17fa596de1e29cfc1bf1bb8d
|
|
BLAKE2b-256 checksum How to use checksums |
6c28c8b513f79d51b616b2b884d67607810db6f70cccebdb9873e16828ba07f3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / myntkaup-0.1.0-py3-none-any.whl
| Download URL | myntkaup-0.1.0-py3-none-any.whl |
|---|---|
| Size | 28.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a4d6d41eb108ae8b1165fc9f2cf6cf78e5aac831411a0c4239c84455fd099a0b
|
|
BLAKE2b-256 checksum How to use checksums |
9a4522f16f9055a6f815384756fda402d322e039bbdfb0ad587d62b2ed8b2fb4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Security for myntkaup 0.1.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.