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 wrong
  • next_step: usually, one sentence on what to do about it, written so a person or an agent can act on it
  • type: the error's name on the wire, like needs_connection
  • status: the HTTP status, when there was one
  • details: 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

ErrorWhenWhat to do
NeedsConnectionthe operation needs an account and the connection has none connected yetsend them connect_url
CredentialDisconnectedthe connection's account was revokedsend them connect_url to reconnect
ApprovalRequiredpolicy holds the call for a personwait on operation, retry with approval_id
SpendCapExceededa key, user or agent cap is used upraise the cap or wait for next month
SpendingPausedyou paused the user's spendingshow them user_message and action; resume them once they've paid
PaymentRequiredyour account can't pay for the callhand a person one of its offers, then retry
PlanRequiredyour plan caps what the call needshand a person one of its offers, then retry
Conflictit already exists, or a conversation is mid-turnuse existing_id, or wait and retry
NotFoundno such object for this account or usercheck the id and the user you act for
PermissionDeniedthe key lacks the scope, or the user can't do thisuse a key with the scope
RateLimitedtoo many requests, a daily send cap is used up, or browsers are at capacitythe SDK waits out the request limit and a browser's start for you; for a send cap, retry_after says when a slot frees
UpstreamErrora 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")