Official Smile ID server-side SDK for Python — V3 APIs.
This project is under active development. It is not yet published to PyPI, and the API is not stable. Do not use it in production yet.
The package and the importable module are both named usesmileid. Python 3.8 or later is required.
pip install usesmileidConstruct one client with your partner ID and API key. The SDK manages authentication for you: it fetches a short-lived token from the Smile ID API, caches it until it expires, and refreshes it automatically. You never handle tokens yourself.
import os
import usesmileid
smile = usesmileid.Client(
partner_id="1234",
api_key=os.environ["SMILE_API_KEY"],
environment="sandbox", # the default
)Partner IDs are displayed zero-padded in the portal (for example 002) but must be passed without leading zeros (2).
The client targets the sandbox by default. Set environment="production" to go live:
sandbox→https://testapi.smileidentity.comproduction→https://api.smileidentity.com
Only sandbox and production are named environments. To reach any other Smile ID environment, pass base_url — it wins over environment:
smile = usesmileid.Client(
partner_id="2",
api_key=os.environ["SMILE_API_KEY"],
base_url="https://your-environment.example.com",
)The value must be an absolute https URL with no query or fragment — anything else raises usesmileid.errors.ValidationError at construction. There is deliberately no way to turn this off: partner credentials and personal data travel on every request. environment must be "sandbox" or "production"; any other value is rejected at construction.
Callback URLs must also be https. The SDK validates default_callback_url when you construct the client, and any per-request callback_url before it sends the request.
| Option | Default | Purpose |
|---|---|---|
default_callback_url |
unset | Used when a call omits callback_url; must be https |
timeout |
30 seconds | Per-request total timeout; each method also accepts a timeout override |
max_retries |
2 | Retries for idempotent operations only (see Retries below) |
http_client |
SDK default | Inject your own httpx.Client for testing or proxies |
Every image parameter (selfie_image, liveness_images, document, document_back, comparison_image) accepts a file path (str or os.PathLike), raw bytes, or an open file object.
All verification submissions need a consent record and the user's details. Build consent with the helper; pass user details as a dict or a usesmileid.UserDetails. At least one of email or phone_number is required — the SDK checks this before sending.
from datetime import datetime, timezone
consent = usesmileid.Consent.granted(
granted_at=datetime.now(timezone.utc),
notice_language="EN",
notice_privacy_policy_url="https://example.com/privacy",
)
user_details = {
"given_names": "Amina Fatou",
"last_name": "Clearwater",
"email": "amina.clearwater@example.com",
}Non-production environments match test identities on given names, last name and email. An identity they do not recognise resolves to block.
The examples below assume smile, consent and user_details are defined as above.
Verify an ID number against the issuing authority.
accepted = smile.enhanced_kyc.verify(
country="NG",
id_type="NIN",
id_number="12345678901",
user_details=user_details,
consent=consent,
)
print(accepted.job_id, accepted.is_accepted)Verify a selfie against a photo of an identity document. id_type is optional; the document type is auto-classified when omitted.
accepted = smile.documents.verify(
selfie_image="selfie.jpg",
liveness_images=["live1.jpg", "live2.jpg", "live3.jpg",
"live4.jpg", "live5.jpg", "live6.jpg"],
document="passport_front.jpg",
country="NG",
user_details=user_details,
consent=consent,
)Same as document verification, but id_type is required and the ID information is also checked against the issuing authority.
accepted = smile.documents.verify_enhanced(
selfie_image="selfie.jpg",
liveness_images=["live1.jpg", "live2.jpg", "live3.jpg",
"live4.jpg", "live5.jpg", "live6.jpg"],
document="license_front.jpg",
document_back="license_back.jpg",
country="NG",
id_type="DRIVERS_LICENSE",
user_details=user_details,
consent=consent,
)Verify a selfie against the photo on file with an ID authority.
accepted = smile.biometric_kyc.verify(
selfie_image="selfie.jpg",
liveness_images=["live1.jpg", "live2.jpg", "live3.jpg",
"live4.jpg", "live5.jpg", "live6.jpg"],
country="NG",
id_type="NIN",
id_number="12345678901",
user_details=user_details,
consent=consent,
)Register a user's selfie for later authentication.
accepted = smile.biometric.enroll(
selfie_image="selfie.jpg",
liveness_images=["live1.jpg", "live2.jpg", "live3.jpg",
"live4.jpg", "live5.jpg", "live6.jpg"],
user_details=user_details,
consent=consent,
user_id="user_01h8x9y2z3a4b5c6d7e8f9g0h1",
)Authenticate a previously enrolled user. Set use_enrolled_image=True to re-use the enrolled image instead of uploading a new selfie.
accepted = smile.biometric.authenticate(
user_id="user_01h8x9y2z3a4b5c6d7e8f9g0h1",
selfie_image="selfie.jpg",
liveness_images=["live1.jpg", "live2.jpg", "live3.jpg",
"live4.jpg", "live5.jpg", "live6.jpg"],
user_details=user_details,
consent=consent,
)Compare a selfie against another image (a document photo, ID photo or portrait).
accepted = smile.biometric.compare(
selfie_image="selfie.jpg",
comparison_image="id_photo.jpg",
comparison_image_type="ID_PHOTO", # DOCUMENT | ID_PHOTO | PORTRAIT
user_details=user_details,
consent=consent,
)status = smile.verifications.retrieve("job_01h8x9y2z3a4b5c6d7e8f9g0h1")
print(status.status) # "processing", "not_found", or the decisionA running job reports status="processing". Once it finishes, status is the decision itself: clear, block, attention or error. message reads "Job completed" on every finished job, so read the decision from status, not from message.
A job that is not found returns a JobStatus with status="not_found" — it does not raise an error, so polling can distinguish "not found yet" cleanly.
Polls the status endpoint while the job is processing or not_found, and returns as soon as it reaches a decision. status.is_complete is true for any decision. Raises usesmileid.errors.TimeoutError if the job does not finish in time.
status = smile.verifications.wait_until_complete(
"job_01h8x9y2z3a4b5c6d7e8f9g0h1",
interval=2.0, # seconds between polls
timeout=60.0, # give up after this many seconds
)
print(status.status) # "clear"By default a not_found status is treated as "not found yet" and polling continues; pass treat_not_found_as_pending=False to return it immediately.
Re-send the callback for a completed verification.
replayed = smile.verifications.replay(
"job_01h8x9y2z3a4b5c6d7e8f9g0h1",
callback_url="https://app.example.com/webhook", # optional override
)Replaying a job that is still processing raises usesmileid.errors.ConflictError.
Flag a user as fraudulent, or clear a previous flag. flag_fraud and clear_fraud are convenience wrappers over report_fraud.
smile.users.flag_fraud(
"user_01h8x9y2z3a4b5c6d7e8f9g0h1",
reason="FIRST_PARTY_FRAUD",
reported_by="risk@example.com",
)
smile.users.clear_fraud(
"user_01h8x9y2z3a4b5c6d7e8f9g0h1",
notes="Cleared after review",
reported_by="risk@example.com",
)reason is required when flagging; notes is required when clearing or when reason="OTHER". The SDK checks these rules before sending.
No authentication required.
banks = smile.services.bank_codes(country="NG")
for bank in banks.bank_codes:
print(bank.code, bank.name)No authentication required.
id_types = smile.services.supported_id_types(country="NG")
for id_type in id_types.id_types:
print(id_type.type, id_type.label)No authentication required.
documents = smile.services.supported_documents(country_code="NG")
for entry in documents.valid_documents:
print(entry.country.name, [d.code for d in entry.id_types])status = smile.services.id_status(country="NG", id_type="NIN")
print(status.last_known_status, status.last_hour_success_rate)Submission endpoints return an AcceptedResponse. Use response.is_accepted rather than comparing the raw status string — the API returns both "Accepted" and "accepted" depending on the endpoint, and is_accepted normalizes the difference.
All errors raised by the SDK subclass usesmileid.errors.SmileIDError and expose status_code, status, message, code, request_id and raw_body.
import usesmileid.errors
try:
accepted = smile.enhanced_kyc.verify(...)
except usesmileid.errors.PaymentRequiredError:
... # top up your wallet
except usesmileid.errors.InvalidRequestError as err:
print(err.status_code, err.message)
except usesmileid.errors.SmileIDError as err:
... # everything else| Error | Raised on |
|---|---|
InvalidRequestError |
HTTP 400, 415 |
ValidationError |
Client-side validation, before any request is sent |
AuthenticationError |
HTTP 401 (after one automatic token refresh) |
PaymentRequiredError |
HTTP 402 |
PermissionError |
HTTP 403 |
NotFoundError |
HTTP 404 |
ConflictError |
HTTP 409 |
PayloadTooLargeError |
HTTP 413 |
RateLimitError |
HTTP 429 |
APIError |
HTTP 5xx |
UnexpectedResponseError |
A success response whose body is not a JSON object |
ConnectionError |
Network failure or timeout, no HTTP response |
TimeoutError |
wait_until_complete deadline reached |
The SDK automatically retries idempotent operations only: status and services reads, and the internal token fetch. Retries cover connection errors and HTTP 408, 429 and 5xx, with exponential backoff, and honour the Retry-After header. HTTP 409 is never retried.
Submission calls (verification, enrollment, authentication, compare, replay, fraud reports) are never retried automatically, because a retry could create a duplicate job. A connection failure on these raises usesmileid.errors.ConnectionError and you decide whether to retry.
Every request carries three telemetry headers: SmileID-Source-SDK, SmileID-Source-SDK-Version and User-Agent. They identify the SDK and its version for observability. They are never used for authentication and carry no personal data.
This project is licensed under the MIT licence. See LICENSE for details.
See SECURITY.md for how to report a vulnerability.