Skip to main content

noresponse

The NoResponse client for Python. It assesses a signup or gateway event, polls a pending assessment, pages through an account's history, records what you later learned, rotates your API key and verifies webhooks.

Python 3.10 or later. Sync and asyncio. Fully typed (py.typed). The client is generated from the NoResponse OpenAPI document; the thin layer on top of it (defaults, response types, errors, wait, webhooks) is hand-written.

pip install noresponse

Quick start

from noresponse import NoResponse

nr = NoResponse()  # reads NORESPONSE_API_KEY

assessment = nr.assessments.create(
    account_id="usr_8f2k1",
    identifiers={"email": "jordan.lee@example.com", "name": "Jordan Lee", "ip": "81.2.69.160"},
)

if assessment.status == "pending":
    # No recommendation yet. Apply your fallback now, then wait or take the webhook.
    assessment = nr.assessments.wait(assessment.assessment_id)

if assessment.risk is not None:
    print(assessment.risk.recommendation)  # allow, step_up or review

Keep the API key on your server. The client raises ValueError at construction without one.

Async code uses the same client with the _async methods:

async with NoResponse() as nr:
    assessment = await nr.assessments.create_async(
        account_id="usr_8f2k1",
        identifiers={"email": "jordan.lee@example.com"},
    )

Configuration

import httpx
from noresponse import NoResponse

nr = NoResponse(
    "nrk_...",                                   # default: NORESPONSE_API_KEY
    server_url="https://api.noresponse.org",     # default; or NORESPONSE_BASE_URL
    timeout_ms=10_000,                           # per request
    retry_config=None,                           # None turns retries off
    client=httpx.Client(proxy="http://proxy:3128"),
)
Argument Default Notes
api_key NORESPONSE_API_KEY A string, or a function that returns one.
server_url NORESPONSE_BASE_URL, then https://api.noresponse.org HTTPS, except localhost.
timeout_ms 10 000 Per request. Every method also takes timeout_ms.
retry_config see Retries Every method except keys.rotate also takes retries.
client, async_client a new httpx.Client and httpx.AsyncClient Your own, for a proxy, mTLS or tests.
debug_logger off; NORESPONSE_LOG=debug logs to the noresponse logger One line per request and response. See Logging.

Use the client as a context manager (with or async with) to close the HTTP clients it opened. Make one client per process and reuse it.

Every method also takes server_url, timeout_ms and http_headers for that call, and every method except keys.rotate takes retries. Rotation is not idempotent, so it is never retried.

Assessments

Create

assessment = nr.assessments.create(
    account_id="usr_8f2k1",                                    # your opaque account id
    identifiers={"email": "jordan.lee@example.com"},           # email, username or phone; name and ip refine the match
    policy_context={"flow": "signup", "region": "AU"},         # signup, login, recovery or profile_change
    client_context={"timezone": "Australia/Sydney", "platform": "web", "form_duration_ms": 1800},
    auth_context=auth_context,                                 # a verified session, see below
    response_mode="standard",                                  # detailed adds signals and risk.details (detailed scope)
    idempotency_key="signup-usr_8f2k1-0001",
)

The answer is the assessment whether the API answered 200 (complete or partial) or 202 (pending). status is complete, partial or pending; risk.recommendation is allow, step_up or review and is absent while pending.

The idempotency key is generated (a UUID) when you leave it out and is sent again on every retry of that call. Pass your own to make a retry of the same signup from your side replay the same assessment: the API answers the latest version with meta.idempotent_replayed true and starts no new work. The same key with a different body answers 409.

Get and wait

latest = nr.assessments.get(assessment.assessment_id)
settled = nr.assessments.wait(assessment.assessment_id, timeout=30)

wait polls get until the status is no longer pending, sleeping the server's Retry-After (at least one second) between polls, and raises APITimeoutError after timeout seconds (default 30). The error is a WaitTimeoutError whose last attribute is the final pending assessment. Pass response_mode="detailed" to either. In async code use get_async and wait_async.

Query an account's history

page = nr.assessments.query(account_id="usr_8f2k1", limit=20)
page.items       # newest first, each at its latest version
page.has_more    # True when another page exists
page.next_page() # the next page, or None

for assessment in page:
    ...  # every assessment across every page

In async code, page = await nr.assessments.query_async(...), then async for assessment in page and await page.next_page_async().

Record an outcome

receipt = nr.assessments.record_outcome(
    assessment.assessment_id,
    outcome="chargeback",  # confirmed_abuse, account_takeover, chargeback, false_positive, legitimate or unknown
    observed_at="2026-10-20T10:00:00Z",
    external_ref="case-42",
)

Outcomes take an idempotency_key the same way create does.

Keys

rotation = nr.keys.rotate(overlap_seconds=3600)
rotation.key                      # the new key, shown once
rotation.previous_key_expires_at  # when the key that signed the request stops working

The default overlap is 24 hours (minimum 60 seconds, maximum 7 days). Rotate with the new key next time.

Response metadata

Every returned object carries meta, which is not part of the body and is left out of model_dump():

assessment.meta.request_id           # X-Request-ID, to quote when you contact us
assessment.meta.retry_after          # seconds, when the API sent Retry-After
assessment.meta.location             # the polling path of a pending assessment
assessment.meta.idempotent_replayed  # True when the answer replays an earlier request

Assessments that arrive by webhook or inside a page have an empty meta.

Errors

Every API error extends NoResponseError and has the same attributes whatever the status: status, type, title, detail, code, request_id, retry_after and, on a 400, invalid_params (the offending fields). str(error) reads HTTP 429 rate_limited: Rate limited. The tenant's assessment rate is exhausted; retry after the Retry-After interval. (request 0123456789abcdef0123456789abcdef). The raw status_code, headers, body and raw_response are there too.

Class When
BadRequestError 400: the body or a header is malformed; see invalid_params and code
AuthenticationError 401: missing, malformed, revoked or expired key
PermissionDeniedError 403: the key lacks the operation's scope
NotFoundError 404: unknown, expired or foreign assessment
ConflictError 409: idempotency key reused with a different body, or a key already rotating
RateLimitError 429: see retry_after
APIStatusError any other 4xx or 5xx, including 503
ResponseValidationError a 2xx the client could not read (code is invalid_response)
APIConnectionError DNS, TLS or connection failure
APITimeoutError the request or a wait timed out; also an APIConnectionError and a TimeoutError
from noresponse import NoResponseError, RateLimitError, APIConnectionError

try:
    nr.assessments.create(account_id="usr_8f2k1", identifiers={"email": email})
except RateLimitError as error:
    schedule(error.retry_after)
except APIConnectionError:  # includes APITimeoutError
    fallback()
except NoResponseError as error:
    log(error.status, error.code, error.request_id)

The two transport errors do not extend NoResponseError, because no response exists to describe.

Retries

Connection errors, 408, 429 and every 5xx are retried with exponential backoff (500 ms, doubling to 8 s between attempts) for at most 30 s in all. When the API sends Retry-After, the client sleeps that long instead. A POST sends the same Idempotency-Key on every attempt, so a retried create or outcome is recorded once. 409 and other 4xx are not retried.

Override per client with retry_config, or per call with retries:

from noresponse.utils import BackoffStrategy, RetryConfig

nr.assessments.create(
    account_id="usr_8f2k1",
    identifiers={"email": email},
    retries=RetryConfig("backoff", BackoffStrategy(200, 2000, 2, 10_000), True),
)
nr.assessments.create(account_id="usr_8f2k1", identifiers={"email": email}, retries=None)  # no retries

Webhooks

Verify the exact bytes you received, then act on the typed event. secrets maps each whk_ key id we give you to its secret; hold both during a rotation.

import os
from noresponse import Webhook, WebhookVerificationError
from noresponse import AssessmentExpired, Assessment

SECRETS = {
    os.environ["NORESPONSE_WEBHOOK_KEY_ID"]: os.environ["NORESPONSE_WEBHOOK_SECRET"],
    # During a rotation, add the previous key id and secret here as well.
}

@app.post("/webhooks/noresponse")  # FastAPI: await request.body() is the raw body
async def handler(request: Request) -> Response:
    try:
        event = Webhook.construct_event(await request.body(), request.headers, SECRETS)
    except WebhookVerificationError:
        return Response(status_code=400)
    if event.type == "endpoint.verification":
        return JSONResponse(Webhook.verification_response(event))
    if isinstance(event.data, AssessmentExpired):
        forget(event.data.assessment_id)  # GET now answers 404
    elif isinstance(event.data, Assessment) and is_newer(event.data):  # keep the highest assessment_version per assessment
        apply(event.data)  # the full assessment, as assessments.get returns it
    return Response(status_code=204)

construct_event(payload, headers, secrets, tolerance_seconds=300) takes the raw body (bytes or the exact text) and any header mapping or list of pairs, and refuses a repeated signature header, a delivery older than 300 seconds, an unknown key id or a bad signature with a WebhookVerificationError whose code says which. Deliveries are at least once: deduplicate on event.id. The same functions are on Webhook, on nr.webhooks and in the noresponse namespace.

Logging

Set NORESPONSE_LOG=debug, or pass debug_logger, to get one line per request and response: method, URL, status and request id. The API key, other headers and bodies are kept out of the log.

Raw responses

nr.assessments.with_raw_response.get(...) returns an object with status_code, headers, request_id and http_response. Its parse() returns the generated envelope, whose result is the model. Use it when you need the status line or every header.

Support

Documentation: https://docs.noresponse.org. Issues: https://github.com/noresponsehq/python/issues. Quote meta.request_id or error.request_id when you write to us.

Apache-2.0.

Metadata

Release files for noresponse 2.0.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for noresponse 2.0.8
File Size Uploaded Provenance
noresponse-2.0.8.tar.gz 107.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for noresponse 2.0.8
File Interpreter ABI Platform
noresponse-2.0.8-py3-none-any.whl Python 3 none any Details

Total release size: 255.9 kB

Release files / noresponse-2.0.8.tar.gz

Download URL noresponse-2.0.8.tar.gz
Size 107.2 kB
Tags Source
SHA-256 checksum
How to use checksums
de155c11704e1dd01fda86dab791ff0ed57f6940a4c299bae5bd7f13e8369181
BLAKE2b-256 checksum
How to use checksums
7149d1b1468107d78d10ffef4075d62e191bf54eea0bec6a80e89b1b6ce074d6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / noresponse-2.0.8-py3-none-any.whl

Download URL noresponse-2.0.8-py3-none-any.whl
Size 148.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d4bda1ab53992b0ecd6250e0edd4c4acd2f153c76831b8cfe88e29453567458a
BLAKE2b-256 checksum
How to use checksums
d5415a84e28e91f8318cb985b11494685d3fae186845e8c227666a266ebb3e12
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Security for noresponse 2.0.8

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

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