scamai
Official Python SDK for the ScamAI detection platform. One
detect() call covers images, video and audio, with typed exceptions for every
refusal, plus account, usage, history and webhooks. Sync and async clients share
the same surface.
Requirements
Python 3.9 or later. Server-side only: an API key grants full access to your account, so keep it out of client-side code and out of version control.
Installation
pip install scamai
Quickstart
from scamai import ScamAI
client = ScamAI() # reads SCAMAI_API_KEY
det = client.detect("suspect.jpg")
print(det["verdict"]) # "LIKELY_AI_MANIPULATED"
print(det["confidence"]) # 0.99
print(det["credits_used"]) # 1
The async client is the same surface, awaited. Use it inside FastAPI, aiohttp or any event loop, where a blocking HTTP call would stall every other request:
from scamai import AsyncScamAI
async with AsyncScamAI() as client:
det = await client.detect("suspect.jpg")
Authentication
Keys are created in the dashboard and shown
once. The client reads SCAMAI_API_KEY from the environment by default:
export SCAMAI_API_KEY=sk_...
Pass it directly when your keys live somewhere else, such as a secrets manager:
client = ScamAI(api_key=secrets.get("scamai"))
Requests are authenticated with the x-api-key header. Authorization: Bearer
is read by the gateway as a dashboard session token and answers 401, so the SDK
never sends it.
The surface
client.detect(file, ...) |
The unified endpoint (POST /v1/detections). file is a path, bytes, an open binary file, or (filename, data, content_type). |
client.detections.create_from_url(url) |
Link intake. The gateway fetches the media. |
client.detections.get(id) |
Read a past detection back. Returns the same envelope the original call answered. |
client.tasks.receipt(task_id) |
The verdict receipt for a past detection. |
client.account.profile / balance / ledger / subscriptions |
Who you are and what you have spent. |
client.keys.list / create / revoke |
API keys. create() regenerates per scope, and the returned value is shown once. |
client.history.list / stats |
Detection history and aggregates. |
client.usage.pricing() |
The per-service price catalog. |
client.webhooks.list / create(url) / remove(id) / test(id) |
detection.completed deliveries, HMAC-signed. |
verify_webhook_signature(raw_body, header, secret) |
Verify X-Scamai-Signature. |
client.request(method, path, ...) |
Escape hatch for any route the typed surface does not cover, with the SDK's auth, error handling and retry rules. |
Responses are plain dicts, verbatim from the wire.
What comes back
One envelope for every media type. The base fields are always present. Video and
audio each add their own, and a field that does not apply to a kind is absent
rather than None, so use det.get(...) instead of comparing against None.
| Field | Kind | |
|---|---|---|
verdict |
all | The routing decision: LIKELY_AUTHENTIC, SUSPICIOUS or LIKELY_AI_MANIPULATED. |
confidence |
all | 0 to 1, or None when the detector did not commit. The only score. Sort a review queue on it. |
summary |
all | One plain-English sentence. |
model |
all | The public label, for example "Eva V1.6". Always a string. |
credits_used |
all | What the ledger actually debited. |
media |
all | {type, filename, mime_type, bytes}. |
id |
all | This detection's id. Pass it to detections.get() to read the run back. |
created_at |
all | ISO 8601, UTC. |
object / status |
all | Always "detection" and "completed" on a synchronous run. |
frames / frames_analyzed |
video | The per-frame series, and its length. |
frames_metered |
video | Frames billed. Not the same as frames_analyzed. |
threshold_used |
video | The line the verdict was decided against. |
duration_ms / segments |
audio | Clip length (the meter), and the per-window timeline. |
zero_charge_reason |
any | Only on a free duplicate run. |
source |
any | Only when the media came from a link. |
Handle all three verdicts. A branch that omits one falls through silently.
Reading a detection back
detect() answers on the same request, so there is no job to poll. A long video
holds the call open until it finishes.
det = client.detect("suspect.jpg")
store(det["id"]) # the handle to this run
# Later, the same envelope again.
again = client.detections.get(det["id"])
Without the id, find the run in the history and read it back from there. The same id also resolves a receipt, which is smaller and carries no PII:
page = client.history.list(limit=20, offset=0)
receipt = client.tasks.receipt(page["history"][0]["task_id"])
history.list() pages with limit and offset, and filters on service_type,
success, start_date, end_date and search.
Webhooks
Register an endpoint, then verify every delivery before you trust it. The signature is computed over the raw request body, so read the body as bytes and verify it before any JSON parsing.
endpoint = client.webhooks.create("https://example.com/hooks/scamai")
# endpoint["secret"] is returned once, here, and never again. Store it now.
X-Scamai-Signature carries t=<unix seconds>,v1=<hex>, where the hex is
HMAC-SHA256(secret, "<t>.<raw_body>"). This is the Stripe scheme, so existing
verification code ports over.
import os
from fastapi import FastAPI, Request, Response
from scamai import verify_webhook_signature, ScamAIError
app = FastAPI()
@app.post("/hooks/scamai")
async def scamai_webhook(request: Request):
try:
event = verify_webhook_signature(
await request.body(), # raw bytes, not a parsed dict
request.headers.get("x-scamai-signature"),
os.environ["SCAMAI_WEBHOOK_SECRET"], # the secret from create()
)
except ScamAIError:
return Response("bad signature", status_code=400)
if event["type"] == "detection.completed":
handle(event["data"])
return Response(status_code=200) # acknowledge fast, do the work off the request
Deliveries older than five minutes are rejected, which bounds replay of a
captured request. client.webhooks.test(endpoint["id"]) sends a delivery so you
can confirm the endpoint before real traffic reaches it;
event.get("test") is true only for those.
A detection.completed delivery carries the run's own words, the same verdict
and confidence detect() returned for it:
event["data"] |
|
|---|---|
taskId |
The detection's id. The same one detections.get() takes. |
verdict |
LIKELY_AUTHENTIC, SUSPICIOUS or LIKELY_AI_MANIPULATED. Absent when the run scored nothing, never a stand-in value. |
confidence |
0 to 1. Absent for the same reason verdict is. |
credits |
What the run was charged. |
data = event["data"]
if data.get("verdict") == "LIKELY_AI_MANIPULATED":
escalate(data["taskId"])
elif data.get("verdict") is None:
pass # the run scored nothing, which is not the same as authentic
Errors
Every failure this package raises is a ScamAIError, so one except covers all
of them:
from scamai import ScamAIError, CreditsError, UnprocessableError
try:
client.detect("suspect.jpg")
except CreditsError as e:
top_up(e.balance)
except UnprocessableError as e:
show_to_user(str(e))
except ScamAIError as e:
log_and_alert(e)
| Exception | Raised on | Also carries |
|---|---|---|
AuthError |
401, and 403 for a bad key | |
ScopeError |
403 with code API_KEY_SCOPE |
|
CreditsError |
402 | balance, document_plan_required, contact |
UnprocessableError |
422. Media we could not judge | reasons |
RateLimitError |
429 | retry_after_seconds |
PlatformError |
5xx | |
APIError |
Any other HTTP status | |
TimeoutError |
The deadline passed | |
ConnectionError |
The host could not be reached | |
MediaError |
The file could not be read, before any request | path |
ConfigError |
Constructed without an API key | |
WebhookVerificationError |
A delivery did not verify |
Everything above subclasses ScamAIError. The HTTP ones subclass APIError and
carry status, code, body and request_id; quote request_id in a support
request. TimeoutError and ConnectionError are ours, not the builtins, so
except ScamAIError still catches them.
Three of these are worth a note:
UnprocessableErroris an answer, not an outage. Show it to your user rather than retrying, and it is never charged.codeisundecodable_image,unsupported_media_typeorlink_not_resolvable, and stays stable where the message does not.TimeoutErrordoes not mean the run did not happen. It may have completed and been billed. Checkhistory.list()before re-sending.MediaErrorandConfigErrorare raised before any request. No call is made, so nothing is charged.
Configuration
client = ScamAI(
api_key=os.environ["SCAMAI_API_KEY"],
base_url="https://api.scam.ai/api", # or SCAMAI_API_BASE
timeout=120.0, # seconds, following httpx
max_retries=2, # reads only, see below
default_headers={"x-source": "review-queue"},
)
timeout is in seconds. timeout_ms is the same deadline in milliseconds and
matches the TypeScript SDK's timeoutMs, so code ported between the two keeps
its meaning. Passing both raises TypeError rather than silently picking one.
Detections are never retried automatically. A retried detect() runs again
and is billed again, so retrying it has to be your decision:
client.detect(file, retry=True) # opt in, knowing the cost
Reads retry on their own, up to max_retries, honouring Retry-After on a 429.
Support
Keys, usage and billing are in the dashboard. For
anything else, contact support and include the
request_id from the error.
Versioning
This package follows semantic versioning. While the major version is 0, a
minor release may change the surface; pin an exact version if that matters to
you.
License
MIT. The TypeScript twin is @scam-ai/sdk.
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 scamai-0.1.0.tar.gz.
File metadata
- Download URL: scamai-0.1.0.tar.gz
- Upload date:
- Size: 18.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b043027d61dfaf9e7a5164f38e1143955b237b501e2452408de09600fa05c118
|
|
| MD5 |
6978f49da7cea2f83c37100a1c6736ff
|
|
| BLAKE2b-256 |
f16eb199890bd393d5b13387fceaf0fe31785ce0f718fdc75147584462790399
|
File details
Details for the file scamai-0.1.0-py3-none-any.whl.
File metadata
- Download URL: scamai-0.1.0-py3-none-any.whl
- Upload date:
- Size: 19.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a442381cc51ba1bd60959e78435a92c3d886be61543b5458fcf8dd52dfb6c47d
|
|
| MD5 |
6c6942ce315ebc9f6a2af28520ef86dd
|
|
| BLAKE2b-256 |
63d7c6edee9653aec457081a697d292f3888e03e9141203161cebdd3310f93c1
|