Skip to main content

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:

  • UnprocessableError is an answer, not an outage. Show it to your user rather than retrying, and it is never charged. code is undecodable_image, unsupported_media_type or link_not_resolvable, and stays stable where the message does not.
  • TimeoutError does not mean the run did not happen. It may have completed and been billed. Check history.list() before re-sending.
  • MediaError and ConfigError are 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

scamai-0.1.0.tar.gz (18.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

scamai-0.1.0-py3-none-any.whl (19.5 kB view details)

Uploaded Python 3

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

Hashes for scamai-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b043027d61dfaf9e7a5164f38e1143955b237b501e2452408de09600fa05c118
MD5 6978f49da7cea2f83c37100a1c6736ff
BLAKE2b-256 f16eb199890bd393d5b13387fceaf0fe31785ce0f718fdc75147584462790399

See more details on using hashes here.

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

Hashes for scamai-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a442381cc51ba1bd60959e78435a92c3d886be61543b5458fcf8dd52dfb6c47d
MD5 6c6942ce315ebc9f6a2af28520ef86dd
BLAKE2b-256 63d7c6edee9653aec457081a697d292f3888e03e9141203161cebdd3310f93c1

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page