Sweet Potato Authentication & Payment Service Python client
Project description
spaps (Python client)
Python SDK for SPAPS-compatible APIs. The distribution name is spaps; the import path is spaps_client.
Examples in this README use placeholders such as user@example.com, admin@example.com, spaps_test_key, and https://api.example.test. Replace them with values from your own deployment.
Install
pip install spaps
This package targets Python 3.9+.
When It Fits
| Need | Package gives you |
|---|---|
| Sync and async clients | SpapsClient and AsyncSpapsClient with a similar surface |
| Typed responses | Pydantic models for auth, sessions, payments, entitlements, webhooks, and more |
| HTTP control points | Retry config, logging hooks, injected httpx clients, and custom token storage |
| Narrow integrations | Standalone helpers for device flow, webhooks, users, email, entitlements, marketing, and permission checks |
Quick Start
from spaps_client import MfaRequiredChallenge, SpapsClient
client = SpapsClient(
base_url="http://localhost:3301",
api_key="spaps_test_key",
)
try:
login = client.auth.sign_in_with_password(
email="user@example.com",
password="correct-horse-battery-staple",
)
if isinstance(login, MfaRequiredChallenge):
tokens = client.auth.mfa_verify(
challenge_id=login.challenge_id,
challenge=login.challenge,
code="123456",
)
else:
tokens = login
bootstrap = client.auth.get_session_context()
session = client.sessions.get_current_session()
products = client.payments.list_products(
category="subscription",
active=True,
limit=5,
)
print(tokens.user.email)
print(bootstrap.tier)
print(session.session_id)
print(products.total)
finally:
client.close()
Async Example
import asyncio
from spaps_client import AsyncSpapsClient
async def main() -> None:
client = AsyncSpapsClient(
base_url="http://localhost:3301",
api_key="spaps_test_key",
)
try:
await client.auth.sign_in_with_password(
email="user@example.com",
password="correct-horse-battery-staple",
)
sessions = await client.sessions.list_sessions()
print(sessions.total)
finally:
await client.aclose()
asyncio.run(main())
Modern Auth Methods
The auth client exposes the active SPAPS auth routes in both sync and async forms:
from spaps_client import MfaRequiredChallenge, SpapsClient
client = SpapsClient(base_url="http://localhost:3301", api_key="spaps_test_key")
methods = client.auth.list_methods()
print([item.method for item in methods.methods if item.enabled])
oidc = client.auth.oidc_nonce()
login = client.auth.oidc_sign_in(
provider="google",
id_token="provider-id-token",
challenge_id=oidc.challenge_id,
)
if isinstance(login, MfaRequiredChallenge):
login = client.auth.mfa_verify(
challenge_id=login.challenge_id,
challenge=login.challenge,
recovery_code="recovery-code",
)
TOTP management and passkey registration are authenticated operations and use stored tokens unless you pass access_token explicitly:
enrollment = client.auth.mfa_totp_enroll()
activation = client.auth.mfa_totp_activate(code="123456")
options = client.auth.webauthn_register_options()
credential = client.auth.webauthn_register_verify(
challenge_id=options.challenge_id,
credential={"id": "browser-produced-credential"},
)
WebAuthn helpers intentionally pass browser-produced credential JSON through to the API; the Python client does not emulate authenticators or perform CTAP operations.
Core Surface
High-Level Clients
| Client | Best for |
|---|---|
SpapsClient |
Sync auth, sessions, payments, usage, access decisions, capability graph inspection, whitelist, secure messages, issue reporting, marketing, metrics, app links, dayrate, skill evals, and support telemetry |
AsyncSpapsClient |
Async auth, sessions, payments, usage, whitelist, secure messages, issue reporting, metrics, entitlements, app links, dayrate, skill evals, and users |
Standalone Helpers
| Helper | Purpose |
|---|---|
EntitlementsClient / AsyncEntitlementsClient |
Resource entitlements, purchase history, manual grants/revokes, and project grants |
UsageClient / AsyncUsageClient |
Secret-key usage authorization and immutable usage recording |
CapabilityClient |
Access decisions, action preparation, decision traces, graph inspection, and contract discovery |
EmailClient / AsyncEmailClient |
Template lookup, preview, and send flows |
UsersClient / AsyncUsersClient |
Batch user/email lookups and app membership administration |
MarketingClient |
Browser-safe marketing event emission and server-side experiment results |
AppLinksClient / AsyncAppLinksClient |
Authenticated short-link creation/update and public link resolution |
DayrateClient / AsyncDayrateClient |
Availability, booking, cancellation, x402 booking holds, and admin booking reads |
DeviceFlowClient |
Device-code login workflows |
PermissionChecker |
Role and admin convenience checks |
verify_spaps_webhook |
Signature verification for incoming SPAPS webhooks |
Configuration
Constructor values override package defaults.
from spaps_client import SpapsClient, RetryConfig, default_logging_hooks
client = SpapsClient(
base_url="https://api.example.test",
api_key="spaps_sec_example",
retry_config=RetryConfig(max_attempts=4, backoff_factor=0.2),
logging_hooks=default_logging_hooks(),
)
Common parameters:
| Parameter | Purpose |
|---|---|
base_url |
Target API origin |
api_key |
Application or service API key |
request_timeout |
Per-request timeout |
token_storage |
Custom token persistence backend |
http_client |
Injected httpx.Client or httpx.AsyncClient |
retry_config |
Retry and backoff policy |
logging_hooks |
Structured request and response logging callbacks |
Common Flows
Agent Access Decisions
client.access.check_access(...) returns a typed decision for both allowed and
denied outcomes. A blocked action is not an exception; inspect allowed,
outcome, and next_actions.
from spaps_client import AccessDecisionRequest, CapabilityError, SpapsClient
client = SpapsClient(
base_url="https://api.example.test",
api_key="spaps_sec_example",
)
client.set_tokens(access_token="admin-access-token")
try:
decision = client.access.check_access(
AccessDecisionRequest.model_validate(
{
"actor": {"actor_type": "user", "actor_ref": "user_123"},
"action": "checkout.create",
"resource": {"resource_type": "product", "resource_ref": "dayrate"},
"controls": {"entitlement_key": "bookme_paid"},
}
)
)
if not decision.allowed:
print(decision.outcome, decision.next_actions)
prepared = client.access.prepare_action(
{
"access": {
"actor": {"actor_ref": "admin_123"},
"action": "admin.delete_user",
"resource": {"resource_type": "user", "resource_ref": "user_123"},
},
"include_command_templates": True,
"environment": "production",
}
)
print(prepared.status, prepared.execution.operator_gate_required)
contract = client.contract.get_contract()
refresh = client.graph.refresh_graph(correlation_id="local-refresh")
nodes = client.graph.list_graph_nodes(node_type="x402_resource", q="dayrate")
explanation = client.access.explain_decision(decision.decision_trace_id)
print(contract.version, refresh.status, refresh.diagnostics.phase2_gate.recommendation if refresh.diagnostics.phase2_gate else None)
print(nodes.projection.projection_status if nodes.projection else None, explanation.graph_node_keys)
except CapabilityError as exc:
print(exc.status_code, exc.code, exc.request_id, exc.diagnostics, exc.remediations)
finally:
client.close()
client.contract.get_contract() includes graph vocabulary fields:
graph_node_types, graph_edge_types, graph_source_domains, and
source_domain_notes. Use them before filtering graph nodes by stable v1 types
such as wallet, api_key, role, and approver.
CapabilityError exposes both Python-native names (status_code,
error_code, request_id) and parity aliases (status, code). Diagnostics
and remediations preserve server-provided details fields.
operator-gated is compatibility/descriptive input when a client supplies it;
it does not authorize mutation command templates. SPAPS returns mutation
command templates only when the request has a server-recognized non-publishable
key and an authenticated admin/operator user context. Publishable callers
cannot self-attest the gate.
Magic Links and Password Reset
from spaps_client import SpapsClient
client = SpapsClient(
base_url="http://localhost:3301",
api_key="spaps_test_key",
)
try:
client.auth.send_magic_link(email="user@example.com")
reset_request = client.auth.request_password_reset(email="user@example.com")
reset_confirm = client.auth.confirm_password_reset(
token="reset-token-from-email",
new_password="Sup3rStrong!",
)
print(reset_request.email, reset_request.sent_at, reset_confirm.message)
finally:
client.close()
Password reset responses use the standard SPAPS success envelope with top-level
success, data, request_id, and timestamp. The Python client returns the
nested data payload: reset requests expose email and sent_at, while reset
confirmation exposes the nested message. A top-level English message is not
canonical and should not drive UI copy. Prefer app-owned success copy, or map
future nested data.message_key/data.status values in your app.
Downstream Adoption
HTMA and future SPAPS consumers should use spaps>=0.6.2 for password reset
flows and stream-aware email sends. Until 0.6.2 is published to PyPI, consume
a local wheel or source checkout from the Sweet Potato commit that carries the
0.6.2 changelog entry.
Release execution is intentionally separate from this handoff: run the manual
Publish Python Client workflow when ready so release automation builds the
already-bumped package metadata and publishes to PyPI. Do not use
npm run publish:python-client, deploy SPAPS, or publish the package as part of
a consumer-only adoption check.
Authenticated Stripe Checkout
create_checkout_session is the convenience path for authenticated checkout.
Pass the server-managed Stripe price_id; the client sends the active Stripe
checkout contract with one line item for that price.
from spaps_client import SpapsClient
client = SpapsClient(
base_url="https://api.example.test",
api_key="spaps_test_key",
)
try:
client.auth.sign_in_with_password(
email="user@example.com",
password="correct-horse-battery-staple",
)
checkout = client.payments.create_checkout_session(
price_id="price_123",
mode="subscription",
success_url="https://app.example.test/success",
cancel_url="https://app.example.test/cancel",
trial_period_days=14,
)
print(checkout.checkout_url)
finally:
client.close()
Server-Side Usage Control
Use usage from a trusted backend with a secret SPAPS key. Browser apps should
call their own backend first; that backend asks SPAPS to authorize the work,
runs the work only on an allow or warning decision, then records the actual
usage with a stable idempotency key.
from spaps_client import SpapsClient
client = SpapsClient(
base_url="https://api.example.test",
api_key="spaps_sec_example",
)
try:
authorization = client.usage.authorize_usage(
feature_key="assistant_tokens",
resource_type="company",
resource_id="company_123",
subject_user_id="user_123",
dimensions={"requests": 1, "input_tokens": 1200},
)
if authorization.decision == "blocked":
message = authorization.reasons[0].message if authorization.reasons else "Usage not authorized"
raise RuntimeError(message)
result = run_assistant_job()
client.usage.record_usage(
authorization_ref=authorization.authorization_ref,
idempotency_key=result.job_id,
feature_key="assistant_tokens",
resource_type="company",
resource_id="company_123",
subject_user_id="user_123",
dimensions={
"input_tokens": result.input_tokens,
"output_tokens": result.output_tokens,
},
metadata={"job_id": result.job_id},
)
finally:
client.close()
For dashboard and policy views, use client.usage.get_features(),
client.usage.get_status(...), and client.usage.get_history(...) from the
same trusted backend context.
Account Memberships and Project Grants
Use these helpers from a trusted backend with a secret SPAPS key. Admin mutation helpers also require an admin access token; do not call membership invitation, project grant, or project revoke helpers from browser code.
from spaps_client import EntitlementsClient, UsersClient
admin_token = "admin-access-token"
users = UsersClient(
base_url="https://api.example.test",
api_key="spaps_sec_example",
access_token=admin_token,
)
entitlements = EntitlementsClient(
base_url="https://api.example.test",
api_key="spaps_sec_example",
access_token=admin_token,
)
try:
users.memberships.add_account_user(
email="teammate@example.com",
capabilities={"projects": ["project_123"]},
metadata={"invited_by": "admin@example.com"},
)
entitlements.project_grants.grant_user(
project_id="project_123",
entitlement_key="pds.project.viewer",
email="teammate@example.com",
reason="Project teammate invite",
)
project_users = entitlements.project_grants.list_project_users(
"project_123",
entitlement_key="pds.project.viewer",
)
user_projects = entitlements.project_grants.list_user_projects("user_123")
access = entitlements.project_grants.check_access(
project_id="project_123",
entitlement_key="pds.project.viewer",
user_id="user_123",
)
print(project_users.count, user_projects.count, access.has_access)
finally:
users.close()
entitlements.close()
Marketing Events
Use client.marketing.emit_event(...) with a publishable key to record anonymous
attribution touches or experiment exposures. Use a secret key from a trusted
server or agent to read experiment results and the current conservative stop
signal.
from spaps_client import SpapsClient
browser_client = SpapsClient(
base_url="https://api.example.test",
api_key="spaps_pub_example",
)
try:
browser_client.marketing.emit_event(
anon_id="anon_01HY...",
event_type="experiment_exposure",
experiment_id="landing-hero-copy",
variant_id="treatment",
dedupe_key="landing-hero-copy:anon_01HY:treatment",
)
finally:
browser_client.close()
agent_client = SpapsClient(
base_url="https://api.example.test",
api_key="spaps_sec_example",
)
try:
results = agent_client.marketing.get_experiment_results("landing-hero-copy")
print(results.decision.recommendation, results.decision.winner_variant_id)
finally:
agent_client.close()
Permission Checks With Explicit Admin Config
from spaps_client import PermissionChecker
checker = PermissionChecker(customAdmins=["admin@example.com"])
role = checker.getRole("operator@example.com")
if checker.requiresAdmin({"email": "operator@example.com"}):
raise PermissionError(
checker.getErrorMessage("admin", role, action="change billing settings")
)
Issue Reporting Voice Token
Voice issue reporting uses a short-lived SPAPS token for ElevenLabs Scribe. Keep the ElevenLabs API key on the SPAPS server.
from spaps_client import SpapsClient
client = SpapsClient(
base_url="https://api.example.test",
api_key="spaps_pub_example",
)
try:
client.set_tokens(access_token="end-user-access-token")
voice_token = client.issue_reporting.create_voice_token()
print(voice_token.provider, voice_token.model_id)
finally:
client.close()
Issue Reporting Screenshot Attachments
Screenshots are uploaded as private pending hosted assets first. Create, update, or reply calls then attach those IDs; raw image bytes, data URLs, and base64 payloads should not be stored in issue notes or target metadata.
from pathlib import Path
from spaps_client import SpapsClient
client = SpapsClient(
base_url="https://api.example.test",
api_key="spaps_pub_example",
)
try:
client.set_tokens(access_token="end-user-access-token")
attachment = client.issue_reporting.upload_issue_report_attachment(
file=Path("protocol-save-failure.png").read_bytes(),
filename="protocol-save-failure.png",
mime_type="image/png",
)
issue = client.issue_reporting.create_issue_report(
target={
"component_key": "patient_protocol_widget",
"component_label": "Patient Protocol Widget",
"page_url": "/patients/123/protocol",
"surface_ref": "daily-log",
"metadata": {"section": "daily log"},
},
note="The save action silently fails after I edit today's protocol note.",
reporter_role_hint="practitioner",
attachment_ids=[attachment.id],
)
access = client.issue_reporting.get_issue_report_attachment_access(
attachment_id=attachment.id,
)
print(issue.id, access.expires_in_seconds)
finally:
client.close()
SPAPS accepts PNG, JPEG, and WebP screenshots up to 10 MiB each, with at most 5 retained screenshots per report. The hosted object remains private; callers fetch a short-lived access URL after normal issue-reporting authorization succeeds. SPAPS does not redact screenshot contents, so host apps should warn users when a capture may include sensitive data.
Skill Evals
Skill eval helpers wrap the SPAPS blind review endpoints for agent-skill logs. Paid case creation accepts a PAYMENT-SIGNATURE through payment_signature. Reviewers submit valuable and not_valuable marks, and submitters read those marks through an insight inbox before applying skill changes.
from spaps_client import SpapsClient
client = SpapsClient(
base_url="https://api.example.test",
api_key="spaps_pub_example",
)
try:
created = client.skill_evals.create_case(
{
"title": "Docs skill comparison",
"task_claim": "Compare both implementations.",
"success_criteria": ["Finds repo boundaries"],
"candidates": [
{
"candidate_id": "A",
"output_ref": "spaps-artifact://case/a",
"evidence_summary": "Validation passed",
"artifact_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"artifact_mime": "text/markdown",
"jsonl_log_ref": "spaps-artifact://logs/a.jsonl",
"jsonl_log_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"skill_ref": "skill://docs-review",
"skill_version_ref": "skill://docs-review/v2.0",
"skill_version_digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
"model_id": "openai/gpt-5.4",
"effort_level": "medium",
"provenance_ref": "skill://private/a",
},
{
"candidate_id": "B",
"output_ref": "spaps-artifact://case/b",
"evidence_summary": "Validation passed",
"artifact_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"artifact_mime": "text/markdown",
"jsonl_log_ref": "spaps-artifact://logs/b.jsonl",
"jsonl_log_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"skill_ref": "skill://docs-review",
"skill_version_ref": "skill://docs-review/v2.1",
"skill_version_digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
"model_id": "openai/gpt-5.4",
"effort_level": "medium",
"provenance_ref": "skill://private/b",
},
],
"case_policy": {
"access_mode": "team_private",
"allowed_model_efforts": [
{"model_id": "openai/gpt-5.4", "effort_level": "medium"}
],
"participant_allowlist": ["reviewer-actor-id"],
},
"idempotency_key": "eval-create-001",
},
payment_signature=signed_payment,
)
room = client.skill_evals.get_review_room(created["case_id"])
print(room["reviewer_state"])
review = client.skill_evals.submit_review(
created["case_id"],
{
"review_marks": [
{
"candidate_id": "B",
"kind": "valuable",
"note": "B checks the configured docs path before recommending an edit.",
"reason_code": "prevents_wrong_repo_patch",
"confidence": "high",
"criterion": "Finds repo boundaries",
}
]
},
)
inbox = client.skill_evals.get_insights(created["case_id"])
print(inbox["valuable"][0]["jsonl_log_ref"], review["review_mark_counts"])
client.skill_evals.respond_to_review(
created["case_id"],
inbox["valuable"][0]["source_review_id"],
{
"response": "applied",
"reason": "Updated the skill from the concrete log-backed insight.",
"applied_insight_ref": inbox["valuable"][0]["insight_ref"],
"skill_change_ref": "skill://docs-review/v2.1",
"skill_version_before": "skill://docs-review/v2.0",
"skill_version_after": "skill://docs-review/v2.1",
"jsonl_log_ref": "spaps-artifact://logs/apply.jsonl",
"jsonl_log_hash": (
"sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
),
"model_id": "openai/gpt-5.4",
"effort_level": "medium",
},
)
finally:
client.close()
Webhook Verification
from spaps_client import verify_spaps_webhook
payload = verify_spaps_webhook(
body=request_body_bytes,
signature=request_headers["X-SPAPS-Signature"],
secret="whsec_example",
)
print(payload.type)
Validation
From the repository root:
npm run lint:python-client
npm run typecheck:python-client
npm run test:python-client
For local package work:
cd packages/python-client
pip install -e '.[dev]'
Troubleshooting
ValueError: Access token not found
Authenticate first with client.auth... helpers, or seed tokens with your configured token storage.
401 or 403 responses
Check the API key, access token, and endpoint role requirements for the target environment.
Hosted examples fail against localhost
Set base_url="http://localhost:3301" and use a local development key or test credentials.
I need more control over HTTP behavior
Inject a custom httpx client or configure RetryConfig and logging hooks.
Which client should I start with?
Use SpapsClient unless your service is already async end to end.
Limitations
- Some specialty integrations live as standalone helpers instead of methods on the main client.
- Endpoint coverage tracks the active SPAPS backend surface; new backend features may appear here incrementally.
- You still need to provide environment-specific API keys, tokens, and deployment URLs.
FAQ
Is the package name different from the import path?
Yes. Install spaps; import from spaps_client.
Does it support async codebases?
Yes. Use AsyncSpapsClient and the async helper classes.
Is webhook verification included?
Yes. Use verify_spaps_webhook.
Can I replace the default token storage?
Yes. Pass a custom token_storage implementation.
Does it retry requests automatically?
It can. Pass RetryConfig if you want retry and backoff behavior.
Metadata
package_name:spapslatest_version:0.6.2minimum_runtime:Python >=3.9api_base_url:https://api.sweetpotato.dev
About Contributions
About Contributions: Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via
ghand independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.
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 spaps-0.6.2.tar.gz.
File metadata
- Download URL: spaps-0.6.2.tar.gz
- Upload date:
- Size: 130.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
79269c1d9600834fc41169fcc8eb96f0bd08f24539fc0c0ec3ab2476c495e0ee
|
|
| MD5 |
f4c167636cdc547837f31b5c68c0abef
|
|
| BLAKE2b-256 |
9b62536717877f8959b50d82c93c9898a064cc2c955b83031cab923a34369cea
|
File details
Details for the file spaps-0.6.2-py3-none-any.whl.
File metadata
- Download URL: spaps-0.6.2-py3-none-any.whl
- Upload date:
- Size: 119.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a2d87078d88632dcff34df130a95254a8e446b7e0eff749266c1f57099efb0a2
|
|
| MD5 |
3ab6141eba54aaee7a95c5428b36bda5
|
|
| BLAKE2b-256 |
6878dca51d130d5974a49741b0742710067a6864691439a79f764d9b8ac53ed3
|