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 SpapsClient
client = SpapsClient(
base_url="http://localhost:3301",
api_key="spaps_test_key",
)
try:
tokens = client.auth.sign_in_with_password(
email="user@example.com",
password="correct-horse-battery-staple",
)
session = client.sessions.get_current_session()
products = client.payments.list_products(
category="subscription",
active=True,
limit=5,
)
print(tokens.user.email)
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())
Core Surface
High-Level Clients
| Client | Best for |
|---|---|
SpapsClient |
Sync auth, sessions, payments, usage, whitelist, secure messages, issue reporting, marketing, metrics, skill evals, and support telemetry |
AsyncSpapsClient |
Async auth, sessions, payments, usage, whitelist, secure messages, issue reporting, metrics, entitlements, skill evals, dayrate, and users |
Standalone Helpers
| Helper | Purpose |
|---|---|
EntitlementsClient / AsyncEntitlementsClient |
Resource entitlements, purchase history, manual grants, and revokes |
UsageClient / AsyncUsageClient |
Secret-key usage authorization and immutable usage recording |
EmailClient / AsyncEmailClient |
Template lookup, preview, and send flows |
UsersClient / AsyncUsersClient |
Batch user and email lookups |
MarketingClient |
Browser-safe marketing event emission and server-side experiment results |
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
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")
client.auth.request_password_reset(email="user@example.com")
client.auth.confirm_password_reset(
token="reset-token-from-email",
new_password="Sup3rStrong!",
)
finally:
client.close()
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.
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.5.0minimum_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.5.1.tar.gz.
File metadata
- Download URL: spaps-0.5.1.tar.gz
- Upload date:
- Size: 105.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6262d2d8f50db680f94910d7d17e5b16c30f2e7d4af199046c943ce8de815f67
|
|
| MD5 |
52961d0b86d4505589c71ecffe7a1dc8
|
|
| BLAKE2b-256 |
0fe37b4906617357ed34500795194573d93342d5d92079652804f246b9152b24
|
File details
Details for the file spaps-0.5.1-py3-none-any.whl.
File metadata
- Download URL: spaps-0.5.1-py3-none-any.whl
- Upload date:
- Size: 98.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 |
bda3ff053c87684cd35db3b723b5bc3265b1787ba9c7f91270d43bb0b3e493e7
|
|
| MD5 |
8b9ae069c03d6f1fa1782c3dc3c33f43
|
|
| BLAKE2b-256 |
5fd0aa0a995245d66583514e69cacbd632dc84ec70485be1c4ca4b8adcf19bae
|