Errors and retries
Every failure is a typed error that says what to do next. Transient ones are retried for you, safely.
Every error says what to do next
Every failure the API reports is raised as an noclick.Error. Each one has:
message: what went wrongnext_step: usually, one sentence on what to do about it, written so a person or an agent can act on ittype: the error's name on the wire, likeneeds_connectionstatus: the HTTP status, when there was onedetails: anything else the API sent, readable as attributes (err.connect_url)request_id: quote it when you contact support
str(err) prints the message and the next step together. Mistakes the SDK catches before sending anything, like an argument of the wrong type, raise Python's own ValueError or TypeError.
import noclick
try:
noclick.Connection.from_name("gmail", user="user_42").run("send_email_message", args)
except noclick.NeedsConnection as err:
ask_user_to_connect(err.connect_url)
except noclick.Error as err:
log.warning("NoClick said: %s", err)The ones you'll handle
| Error | When | What to do |
|---|---|---|
NeedsConnection | the operation needs an account and the connection has none connected yet | send them connect_url |
CredentialDisconnected | the connection's account was revoked | send them connect_url to reconnect |
ApprovalRequired | policy holds the call for a person | wait on operation, retry with approval_id |
SpendCapExceeded | a key, user or agent cap is used up | raise the cap or wait for next month |
SpendingPaused | you paused the user's spending | show them user_message and action; resume them once they've paid |
PaymentRequired | your account can't pay for the call | hand a person one of its offers, then retry |
PlanRequired | your plan caps what the call needs | hand a person one of its offers, then retry |
Conflict | it already exists, or a conversation is mid-turn | use existing_id, or wait and retry |
NotFound | no such object for this account or user | check the id and the user you act for |
PermissionDenied | the key lacks the scope, or the user can't do this | use a key with the scope |
RateLimited | too many requests, a daily send cap is used up, or browsers are at capacity | the SDK waits out the request limit and a browser's start for you; for a send cap, retry_after says when a slot frees |
UpstreamError | a service refused a run (424: its words in message, its HTTP status in upstream_status), or a service such as a model's provider failed (502) | fix what the service objected to (it isn't retried); a failure is retried for you, and after that usually works in a moment |
The Error reference lists every type with its HTTP status.
Money refusals carry their way past
A PaymentRequired or PlanRequired in your account's own scope lists offers: what would lift it, each a link a person opens to pay, with no sign-in. A plan that includes what was refused, an amount of funds, or on the older plans a monthly top-up. Paying stays a person's act; your code, or an agent, hands them the link.
try:
image = noclick.AI.image("A lighthouse at dusk, watercolor")
except noclick.PaymentRequired as short:
offer = short.offers[0]
send_to_owner(f"{offer.label}: ${offer.price_usd} at {offer.url}")
offer.operation.wait() # returns once it's paid
image = noclick.AI.image("A lighthouse at dusk, watercolor")Each offer is a payment operation: it waits on a person until the link is paid (it succeeds, and a payment.completed event is sent) or its time runs out (it expires). Refused again while a link is still open, the same link comes back rather than a new one. A user's scope, or a page's visitor, is never offered the account's links.
Retries are automatic
The SDK retries a request that times out, is rate limited (429) or hits a server error (500, 502, 503 or 504), and a request that can't reach the API at all. It backs off exponentially between attempts, and when the API says how long to wait (Retry-After), it waits that long, up to a minute. It makes up to 3 retries by default:
client = noclick.Client(max_retries=5, timeout=30)Two things aren't retried: a spent quota (noclick.QuotaExceeded), which more attempts won't fix, and a POST that failed with a 500, which the API already remembers (see below).
Retries never do the work twice
Every POST carries an idempotency key, and every retry of it reuses the same key. The API remembers the response to each key for 24 hours, so a retried request returns the first response instead of running again, even if the first attempt's response never reached you. Two cases differ:
- A request the API refused (a 4xx) changed nothing, so it isn't remembered, and a retry runs again.
- A retry that arrives while the first attempt is still running fails with
noclick.Conflict. Retry again shortly to get the first attempt's response.
Other writes (PUT, PATCH, DELETE) set a final state, so repeating one is harmless, though a delete whose first attempt went through reports noclick.NotFound the second time.
To make your own retries safe across processes, pass your own key where a method takes one:
noclick.User.create("user_42", email="ana@example.com", idempotency_key=f"signup-{signup_id}")An idempotency key belongs to the user the request acts for: reusing it for a request that acts for a different user fails with noclick.Conflict.
Rate limits
Each key can make 50 requests a second, with bursts up to 100. Over that, requests get 429 with a Retry-After, which the SDK waits out and retries.
Timeouts
A request times out after 60 seconds by default (timeout= on the client). Waiting on an operation is separate: wait(timeout=...) raises noclick.OperationTimeout when time runs out, and leaves the operation running.
Tell us
Something broke, or something is missing: noclick.feedback reaches the NoClick team as the app's feedback button does. kind is bug, idea or general (the default). Any key can send it, an agent's included, up to 20 notes a day for an account (noclick.RateLimited past that); quote a request_id if you have one.
noclick.feedback("Agent.list could filter by workflow.", kind="idea")