Unofficial Python client for TIC Identity (id.tic.io) — BankID authentication and digital signing
Project description
TIC Identity Python
Unofficial Python client for TIC Identity - Swedish BankID authentication and digital signing. Not affiliated with or endorsed by TIC Identity.
Async-first, built on httpx, websockets, and pydantic.
Installation
pip install tic-identity-python
Requires Python 3.11+.
Quick Start - Authentication
import asyncio
from tic import TicClient, TicHub
async def authenticate(user_ip: str):
async with TicClient(api_key="your-api-key") as client:
# 1. Start a BankID auth session
session = await client.start_auth(user_ip)
print(f"Session: {session.session_id}")
print(f"Auto-start token: {session.auto_start_token}")
# 2. Connect to the real-time hub
hub = TicHub()
await hub.connect()
# 3. Register event handlers
hub.on_status_changed(lambda s: print(f"Status: {s.status} ({s.hint_code})"))
hub.on_completed(lambda c: print(f"Authenticated: {c.user.name} ({c.user.personal_number})"))
hub.on_failed(lambda f: print(f"Failed: {f.hint_code} - {f.message}"))
# 4. Subscribe to session updates
await hub.subscribe(session.session_id, session.subscription_token)
# 5. Optionally poll QR codes for desktop flows
qr = await client.get_qr(session.session_id)
print(f"QR data: {qr.qr_data}")
# Keep listening until completed/failed
await asyncio.sleep(60)
await hub.disconnect()
asyncio.run(authenticate("192.168.1.1"))
Digital Signing
async def sign_document(user_ip: str):
async with TicClient(api_key="your-api-key") as client:
session = await client.start_sign(
user_ip,
user_visible_data="I agree to the terms of service.",
user_visible_data_format="simpleMarkdownV1", # optional
user_non_visible_data="contract-id:abc123", # optional
)
# Use the hub for real-time updates (same as auth)
hub = TicHub()
await hub.connect()
hub.on_completed(lambda c: print(f"Signed by {c.user.name}"))
await hub.subscribe(session.session_id, session.subscription_token)
# Or poll via REST
result = await client.collect(session.session_id)
if result.status == "complete":
print(f"Signature: {result.signature.value}")
print(f"OCSP response: {result.signature.ocsp_response}")
Webhook Verification
When using webhook_url in start_auth or start_sign, verify incoming webhooks with your webhook secret:
from tic import verify_signature, TicWebhookError
def handle_webhook(request):
try:
payload = verify_signature(
payload=request.body,
timestamp=request.headers["X-Ormeo-Timestamp"],
signature=request.headers["X-Ormeo-Signature"],
secret="your-webhook-secret",
max_age_seconds=300, # default
)
print(f"Event: {payload.event}")
print(f"Data: {payload.data}")
except TicWebhookError as e:
print(f"Verification failed: {e}")
The verify_signature function checks the HMAC-SHA256 signature, rejects stale timestamps, and returns a parsed WebhookPayload.
Typed Webhook Parsing
After verifying a webhook, use parse_webhook_data() to get a typed model instead of a raw dict:
from tic import verify_signature, parse_webhook_data, AuthCompletedData, SignCompletedData
payload = verify_signature(payload=body, timestamp=ts, signature=sig, secret=secret)
data = parse_webhook_data(payload)
if isinstance(data, AuthCompletedData):
print(f"Auth completed: {data.user.name} ({data.user.personal_number})")
elif isinstance(data, SignCompletedData):
print(f"Signed by: {data.user.name}")
print(f"Signature: {data.signature.value}")
Returns AuthCompletedData for auth.completed, SignCompletedData for sign.completed, or the raw dict for unknown events.
Enrichment API
Request enrichment data (SPAR, company roles, property ownership, income, IP intelligence) after a completed session:
from tic import TicClient, EnrichmentType, EnrichmentStatus
async with TicClient(api_key="your-api-key") as client:
# 1. Start enrichment after a completed auth session
enrichment = await client.start_enrichment(
session_id="your-session-id",
types=[EnrichmentType.SPAR, EnrichmentType.COMPANY_ROLES],
webhook_url="https://example.com/webhook", # optional
)
print(f"Enrichment ID: {enrichment.enrichment_id}")
print(f"Status: {enrichment.status}")
# 2. Check status (or wait for webhook)
status = await client.get_enrichment_status(enrichment.enrichment_id)
if status.status == EnrichmentStatus.COMPLETED:
# 3. Retrieve the data via the secure URL token
data = await client.get_enrichment_data(token=status.secure_url.split("/")[-1])
if data.spar:
print(f"Name: {data.spar.Namn_Fornamn} {data.spar.Namn_Efternamn}")
if data.company_roles:
for role in data.company_roles:
print(f" {role.legal_name} - {role.position_descriptions}")
# 4. List available enrichment types for your tenant
types = await client.get_enrichment_types()
for t in types:
print(f"{t.type}: {t.description} (enabled={t.enabled})")
Available enrichment types: SPAR, CompanyRoles, PropertyOwnership, Income, IpIntelligence, Full.
Data Verification API
Look up Swedish companies, credit scores, and signing authority without requiring a BankID session:
from tic import TicClient
async with TicClient(api_key="your-api-key") as client:
# Company lookup by country code + registration number
company = await client.get_company("SE", "5566112233")
print(f"{company.data.company_name} ({company.data.activity_status})")
print(f"VAT registered: {company.data.is_registered_for_vat}")
print(f"Representatives: {len(company.data.representatives or [])}")
# Credit score
credit = await client.get_credit("SE", "5566112233")
print(f"Credit score: {credit.data.credit_score}/100")
print(f"Risk forecast: {credit.data.risk_forecast}%")
# Signing authority analysis (AI-generated)
authority = await client.get_signing_authority("SE", "5566112233")
print(f"Summary: {authority.data.analysis.summary}")
for person in authority.data.analysis.eligible_persons:
print(f" {person.name} - can sign alone: {person.can_sign_alone}")
All three endpoints also have _by_id variants that accept an internal company_id:
company = await client.get_company_by_id("12345")
credit = await client.get_credit_by_id("12345")
authority = await client.get_signing_authority_by_id("12345")
QR Code Generation
Generate BankID animated QR data locally, without polling the server:
import time
from tic import TicClient, generate_qr_data
async with TicClient(api_key="your-api-key") as client:
session = await client.start_auth(user_ip)
# Generate QR data client-side using tokens from the session
start = time.monotonic()
while True:
elapsed = int(time.monotonic() - start)
qr = generate_qr_data(
qr_start_token=session.qr_start_token,
qr_start_secret=session.qr_start_secret,
elapsed_seconds=elapsed,
)
render_qr_image(qr) # your QR rendering code
time.sleep(1)
The returned string has the format bankid.<token>.<elapsed>.<hmac> and should be refreshed every second.
BankID Helpers
Utility functions for building BankID URLs and parsing callbacks:
from tic import bankid_autostart_url, hosted_login_url, parse_callback
# Desktop BankID autostart (bankid:/// scheme)
url = bankid_autostart_url(
auto_start_token=session.auto_start_token,
redirect_url="https://example.com/callback",
)
# -> bankid:///?autostarttoken=...&redirect=...
# Mobile BankID autostart (https://app.bankid.com/)
mobile_url = bankid_autostart_url(
auto_start_token=session.auto_start_token,
redirect_url="https://example.com/callback",
mobile=True,
)
# Hosted-mode login redirect
login_url = hosted_login_url(
base_url="https://id.tic.io",
tenant="my-tenant",
callback="https://example.com/callback",
state="my-state",
lang="sv",
)
# Parse callback URL after authentication
result = parse_callback("https://example.com/callback?session_id=abc&token=xyz&state=my-state")
print(result) # {"session_id": "abc", "token": "xyz", "state": "my-state"}
Messages Endpoint
Retrieve localized BankID status messages:
from tic import TicClient
async with TicClient(api_key="your-api-key") as client:
messages = await client.get_messages(language="sv")
for msg in messages:
print(f"{msg.hint_code}: {msg.message}")
# outstandingTransaction: Starta BankID-appen...
Constants and Enums
Typed enums for hint codes, session statuses, error codes, webhook events, and headers:
from tic import HintCode, ErrorCode, SessionStatusEnum, WebhookEvent, WebhookHeader, Environment
# BankID hint codes with semantic helpers
if hint == HintCode.USER_SIGN:
print("User is entering security code")
print(HintCode.EXPIRED_TRANSACTION.is_failed) # True
print(HintCode.STARTED.is_pending) # True
# Session status
if status == SessionStatusEnum.COMPLETE:
collect_result()
# API error codes
try:
await client.start_auth(ip)
except TicAPIError as e:
if e.code == ErrorCode.LIMIT_EXCEEDED:
print("Monthly limit reached")
# Webhook events
if payload.event == WebhookEvent.ENRICHMENT_COMPLETED:
fetch_enrichment_data()
# Webhook header names
sig = headers[WebhookHeader.SIGNATURE] # "X-Ormeo-Signature"
ts = headers[WebhookHeader.TIMESTAMP] # "X-Ormeo-Timestamp"
# Environment base URLs
client = TicClient(api_key="key", base_url=Environment.PRODUCTION)
REST Client Methods
TicClient is an async context manager. All methods are coroutines.
| Method | Description |
|---|---|
start_auth(end_user_ip, *, provider, personal_number, callback_url, webhook_url, state) |
Start a BankID auth session. Returns AuthSession. |
start_sign(end_user_ip, user_visible_data, *, provider, user_visible_data_format, user_non_visible_data, personal_number, callback_url, webhook_url, state) |
Start a BankID signing session. Returns AuthSession. |
collect(session_id) |
Collect the final result (user, signature, token). Returns CollectResult. |
get_status(session_id) |
Get current session status. Returns SessionStatus. |
poll(session_id) |
Long-poll for status change. Returns SessionStatus. |
get_qr(session_id) |
Get animated QR code data. Returns QRCodeResult. |
cancel(session_id) |
Cancel a session. Returns bool. |
extend(session_id) |
Extend session timeout. Returns ExtendResult. |
get_usage(year, month) |
Get monthly usage stats. Returns UsageStats. |
get_messages(language) |
Get localized BankID messages. Returns list[LocalizedMessage]. |
start_enrichment(session_id, types, *, webhook_url, state) |
Start enrichment for a completed session. Returns EnrichmentResponse. |
get_enrichment_status(enrichment_id) |
Check enrichment progress. Returns EnrichmentResponse. |
get_enrichment_types() |
List available enrichment types. Returns list[EnrichmentTypeInfo]. |
get_enrichment_data(token) |
Retrieve enrichment data. Returns EnrichmentData. |
get_company(country_code, reg_nr) |
Look up company info. Returns CompanyLookupResponse. |
get_company_by_id(company_id) |
Look up company by internal ID. Returns CompanyLookupResponse. |
get_credit(country_code, reg_nr) |
Get company credit score. Returns CompanyCreditResponse. |
get_credit_by_id(company_id) |
Get credit by internal ID. Returns CompanyCreditResponse. |
get_signing_authority(country_code, reg_nr) |
Analyse signing authority. Returns SigningAuthorityResponse. |
get_signing_authority_by_id(company_id) |
Signing authority by internal ID. Returns SigningAuthorityResponse. |
You can also inject your own httpx.AsyncClient:
client = TicClient(api_key="key", http_client=my_httpx_client)
SignalR Hub Events
TicHub connects to the real-time SignalR hub over WebSocket. After calling hub.connect() and hub.subscribe(session_id, subscription_token), the following events fire:
| Helper | Event | Payload |
|---|---|---|
on_qr_code(handler) |
OnQRCode |
str - animated QR code data |
on_status_changed(handler) |
OnStatusChanged |
StatusChanged(status, hint_code, message, message_en) |
on_completed(handler) |
OnCompleted |
Completed(session_id, status, user, completed_at) |
on_failed(handler) |
OnFailed |
Failed(hint_code, message, message_en) |
on_cancelled(handler) |
OnCancelled |
No arguments |
on_timeout_warning(handler) |
OnTimeoutWarning |
TimeoutWarning(seconds_remaining, can_extend) |
on_order_regenerated(handler) |
OnOrderRegenerated |
OrderRegenerated(order_count, max_orders, session_expires_in_seconds) |
Typed Hub Responses
Hub invocable methods return typed pydantic models:
hub = TicHub()
await hub.connect()
# subscribe() returns SubscribeResponse
sub = await hub.subscribe(session_id, subscription_token)
print(f"Provider: {sub.provider}, Status: {sub.status}")
print(f"Expires: {sub.session_expires_at}")
# get_status() returns AuthStatusResponse
status = await hub.get_status()
if status:
print(f"{status.status} / {status.hint_code}")
print(f"Orders: {status.order_count}/{status.max_orders}")
# extend_session() returns ExtendResult
ext = await hub.extend_session()
print(f"Extended: {ext.success}, new expiry: {ext.new_expires_at}")
| Method | Description | Return type |
|---|---|---|
subscribe(session_id, subscription_token) |
Subscribe to a session's events | SubscribeResponse |
cancel_session() |
Cancel the active session | None |
get_qr_code() |
Request current QR code data | str | None |
get_status() |
Request current status | AuthStatusResponse | None |
extend_session() |
Extend session timeout | ExtendResult |
Handlers can be sync or async - async handlers are automatically scheduled as tasks.
Exceptions
| Exception | When |
|---|---|
TicAPIError |
HTTP error from the REST API. Has status_code, code, message, details. |
TicHubError |
SignalR hub connection or invocation error. |
TicWebhookError |
Webhook signature verification failure. |
TicError |
Base class for all of the above. |
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tic_identity_python-0.2.1.tar.gz.
File metadata
- Download URL: tic_identity_python-0.2.1.tar.gz
- Upload date:
- Size: 72.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
576cc00d3e4848209e8ba57dd551bdb857aeeedee552b2724f4b1b7b803d997e
|
|
| MD5 |
94c5c0aa3e3dd3a8d9b4d0eec4f44f82
|
|
| BLAKE2b-256 |
17694f9036a021e9c5f42ac33bf018e187c0f2e22e54d354fbea7c7a2d0352fb
|
Provenance
The following attestation bundles were made for tic_identity_python-0.2.1.tar.gz:
Publisher:
publish.yml on liamlenholm/tic-identity-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tic_identity_python-0.2.1.tar.gz -
Subject digest:
576cc00d3e4848209e8ba57dd551bdb857aeeedee552b2724f4b1b7b803d997e - Sigstore transparency entry: 1390242682
- Sigstore integration time:
-
Permalink:
liamlenholm/tic-identity-python@118c484db3f729f3c4b11610c83f5538df9a3350 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/liamlenholm
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@118c484db3f729f3c4b11610c83f5538df9a3350 -
Trigger Event:
release
-
Statement type:
File details
Details for the file tic_identity_python-0.2.1-py3-none-any.whl.
File metadata
- Download URL: tic_identity_python-0.2.1-py3-none-any.whl
- Upload date:
- Size: 25.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
07c1d9034b424c40cb125009abe7564ce040572300d6c9460c07477454d66c16
|
|
| MD5 |
c0c01213d0ca209ef639b6763bd12259
|
|
| BLAKE2b-256 |
0c86881d4e142d7058c070b81347d600901d6d3efd6164032ce4ab43c2e33f15
|
Provenance
The following attestation bundles were made for tic_identity_python-0.2.1-py3-none-any.whl:
Publisher:
publish.yml on liamlenholm/tic-identity-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tic_identity_python-0.2.1-py3-none-any.whl -
Subject digest:
07c1d9034b424c40cb125009abe7564ce040572300d6c9460c07477454d66c16 - Sigstore transparency entry: 1390242693
- Sigstore integration time:
-
Permalink:
liamlenholm/tic-identity-python@118c484db3f729f3c4b11610c83f5538df9a3350 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/liamlenholm
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@118c484db3f729f3c4b11610c83f5538df9a3350 -
Trigger Event:
release
-
Statement type: