Skip to main content

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: httpx and cryptography.
  • Myntkaup (synchronous) and AsyncMyntkaup (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 for trades.create, giftcards.create, withdrawals.create_bank and withdrawals.create_crypto, and for quotes.create only if you pass idempotency_key.
  • Never retried: 400, 401, 403, 404, 409, 422 and 500, and 429 key_suspended.
  • A Retry-After of more than 60 seconds isn't waited for: the error is raised, with retry_after set.

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)

Source distribution for myntkaup 0.1.0
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)

Table of built distributions (wheels) for myntkaup 0.1.0
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page