Skip to main content

x-tkn

Python client for X-TKN — stateful tokens as a service. Mint a token, hand the code to someone, and control how many times and for how long it can be redeemed.

Get an API key by signing up at x-tkn.com.

pip install x-tkn

Python 3.10+. One dependency, httpx, which is what gives you a sync and an async client from the same package.

Quickstart

from x_tkn import xtkn

token = xtkn.create_token(
    type="password-reset",
    ref_id=user.id,
    max_uses=1,
    expires_in={"minutes": 30},
)

# token.code is returned once and never again. Persist or send it now.
send_email(user.email, f"https://app.example.com/reset?c={token.code}")

…and on the other side:

from x_tkn import xtkn, XTknGoneError, XTknNotFoundError

try:
    token = xtkn.redeem_token(code, ref_id=user.id, type="password-reset")
    reset_password(user, token.payload)
except XTknGoneError:
    return render("That link has already been used.")
except XTknNotFoundError:
    return render("That link is not valid.")

Async

AsyncXTkn has the same methods, the same arguments and the same validation — the request building is shared code, not a parallel implementation.

from x_tkn import AsyncXTkn

async with AsyncXTkn() as client:
    token = await client.create_token(max_uses=1, expires_in={"minutes": 15})

In a web application, hold one client for the process rather than building one per request — a client built per request throws its connection pool away each time. Close it on shutdown:

client = AsyncXTkn()

@app.on_event("shutdown")
async def close_xtkn():
    await client.aclose()

The module-level xtkn is sync-only and deliberately so. An AsyncClient that nothing closes is a resource leak the caller cannot see, so the async client is yours to construct and close.

Authentication

The key is read on every request, so it can be set after import.

X_TKN_API_KEY=your_key_here

Or pass it explicitly — necessary if you talk to more than one account:

from x_tkn import XTkn

client = XTkn(api_key=os.environ["MY_KEY"])

Keep the key server-side. It grants full token CRUD for your account.

Options

Option Default
api_key X_TKN_API_KEY, then X_TKN_API_KEY_ID Resolved per request
base_url https://api.x-tkn.com
timeout 30.0 Seconds. None or 0 disables
headers Merged into every request; cannot override Authorization
transport httpx's default For tests, proxies or retries

UNSET versus None

JSON has two ways to say nothing and the API means different things by them: an absent key takes the server's default, an explicit null clears the field. Python's None can only carry one of those, so every optional argument defaults to UNSET and None is left to mean null.

xtkn.update_token(code)                   # changes nothing
xtkn.update_token(code, max_uses=None)    # clears the ceiling — unlimited uses

You rarely have to name UNSET yourself. It matters when you are forwarding values you may or may not have:

from x_tkn import UNSET

xtkn.create_token(ref_id=user.id if user else UNSET)

Methods

Every method raises on failure — see Errors. code is the token's public identifier, returned by create_token.

create_token(...)

token = xtkn.create_token(
    type="handoff",              # [a-z0-9_-], ≤64 chars, defaults to "generic"
    ref_id="user_123",           # your own identifier, ≤256 chars
    payload={"role": "admin"},   # JSON-encoded, ≤64 KB
    max_uses=1,                  # 1–1,000,000. omit for unlimited
    description="Admin invite",  # operator-facing note
    expires_in={"hours": 2},     # or expires_at=datetime(...) — not both
)

token.code is the only copy. The server stores sha256(code) and cannot return it later; every subsequent read leaves the field None.

With no expires_at or expires_in, the server expires the token 30 days after creation.

expires_in takes a timedelta as readily as a dict — timedelta(hours=2) and {"hours": 2} are the same request.

read_token(code)

Returns the token without consuming a use. Check is_active, is_expired, is_used.

redeem_token(code, ...)

Consumes one use and returns the token. Raises XTknGoneError if it is revoked, expired or exhausted; XTknNotFoundError if the code is wrong.

xtkn.redeem_token(code, ref_id=user.id, type="handoff")

Pass both ref_id and type where you can. ref_id confines the lookup to the identity the code was issued for, which is what bounds guessing; type stops the redemption consuming a different kind of code held by the same identity.

update_token(code, ...)

Changes type, ref_id, payload, max_uses or the expiry. description is not updatable — the server's update handler ignores it.

extend_expiration(code, duration)

Moves the expiry to duration from now, not from the existing expiry — so this revives an already-expired token rather than extending from a past date.

xtkn.extend_expiration(session_code, {"hours": 2})

revoke_token(code) / revoke_tokens(...)

xtkn.revoke_token(code)

# Log a user out everywhere. One call revokes a bounded batch.
while xtkn.revoke_tokens(ref_id=user.id, type="session").has_more:
    pass

revoke_tokens requires type or ref_id. An empty filter means "revoke everything", which is not allowed by omission.

delete_token(code)

Permanent. Prefer revoke_token — a revoked token can still explain why a redemption failed, a deleted one is indistinguishable from one that never existed.

list_tokens(...)

result = xtkn.list_tokens(
    ref_id="user_123",
    is_revoked=False,
    sort="-createdAt",
    page=1,
    limit=50,  # capped at 100
)

for token in result:          # TokenList iterates its tokens
    print(token.display_name)

print(result.count)           # total matching the filter, not len(result)

Only type, ref_id and is_revoked are filterable. On the raw API the server silently drops anything else, so a typo widens the result rather than erroring; here it is a TypeError before anything is sent.

sort accepts createdAt, updatedAt, expiresAt, lastUsedAt or uses, each optionally prefixed with -. Anything else is silently replaced with -createdAt by the server; the TokenSort literal type catches it in a type checker first.

Codes are not exposed as a filter. The API accepts one and hashes it to match sha256(code), but it can only return the single token you already hold the code for — read_token(code) does that directly.

The Token object

Attributes are snake_case and timestamps are parsed into aware datetimes. Whatever the server actually sent is on token.raw, unrenamed and unparsed, so a field this SDK does not know about is still reachable.

token.expires_at            # datetime | None
token.raw["expiresAt"]      # "2026-09-30T12:00:00.000Z"

repr(token) masks code, because reprs reach logs and tracebacks far more readily than a deliberate print. Attribute access still returns the real value.

Errors

Every non-2xx response raises. Catch XTknError for all of them, or a subclass to tell them apart.

Class Status Means
XTknRequestError 400 Malformed request or failed validation
XTknAuthError 401, 403 Key missing, unknown or revoked
XTknNotFoundError 404 No such token on this account
XTknGoneError 410 Exists but spent: revoked, expired or out of uses
XTknRateLimitError 429 Hourly guard or monthly quota exhausted
XTknServerError 5xx
XTknConnectionError Never reached the server: DNS, reset, timeout
XTknConfigError Bad arguments; raised before any request

Each carries status, and details when the API supplied a field-level map.

Payload encryption

The server never needs to read your payload, and accounts with requireEncryptedPayload set reject anything that is not already an xtkn.v1. or xtkn.v1r. envelope.

This SDK does not encrypt for you. Pass an already-encrypted string as payload if your account enforces it.

Differences from the JavaScript SDK

The two clients cover the same API and are deliberately close, but they are not transliterations of each other.

@fennecstudio/x-tkn-js x-tkn
timeoutMs, milliseconds timeout, seconds
fetch injection transport injection
Options objects ({ maxUses: 1 }) Keyword arguments (max_uses=1)
listTokens({ where: {...} }) list_tokens(ref_id=..., is_revoked=...) — flattened
Returns plain objects, camelCase Returns dataclasses, snake_case, with .raw for the wire
Timestamps are ISO strings Timestamps are datetime
undefined omits, null sends null UNSET omits, None sends null
Async only XTkn and AsyncXTkn

x-tkn 2.0.0 and @fennecstudio/x-tkn-js 2.x ship against the same generation of the API. The two are versioned independently from here, so do not read matching major numbers as a promise they stay matched.

Development

python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/pytest          # unit tests
.venv/bin/mypy            # strict type check

Tests live in __tests__/ and are named <module>.<function>.unit.py, matching the monorepo's convention rather than pytest's default test_*.py glob — see the python_files setting in pyproject.toml.

License

ISC

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

x_tkn-2.0.0.tar.gz (30.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

x_tkn-2.0.0-py3-none-any.whl (22.2 kB view details)

Uploaded Python 3

File details

Details for the file x_tkn-2.0.0.tar.gz.

File metadata

  • Download URL: x_tkn-2.0.0.tar.gz
  • Upload date:
  • Size: 30.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for x_tkn-2.0.0.tar.gz
Algorithm Hash digest
SHA256 bacbe478eeb229b12e36c4d198ec887b75d37899a29e953be35d07f75f3c4bbf
MD5 6a126f52a7627b48c73caecf4a4374a6
BLAKE2b-256 70277639ffd4de007188531c2200e7970aa83e06bba568f80a283d9daa20a8a6

See more details on using hashes here.

Provenance

The following attestation bundles were made for x_tkn-2.0.0.tar.gz:

Publisher: publish-x-tkn-python.yml on fennecstudio/platform

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file x_tkn-2.0.0-py3-none-any.whl.

File metadata

  • Download URL: x_tkn-2.0.0-py3-none-any.whl
  • Upload date:
  • Size: 22.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for x_tkn-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cbc69388047070c47b9dbacb16789a0b4e9a8de8a33adf14a7a6f7031bfa6564
MD5 986fc830c32c4ec0c6f85bf3410ec57e
BLAKE2b-256 b15cf0af47cadf29e230a2acd33731697758be2543a4c75a18c893fda0c23ed8

See more details on using hashes here.

Provenance

The following attestation bundles were made for x_tkn-2.0.0-py3-none-any.whl:

Publisher: publish-x-tkn-python.yml on fennecstudio/platform

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 files

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