Client libraries
Python
auray-client: a Client and an AsyncClient over httpx, with the same surface as the JavaScript client.
pip install auray-clientv0.3.0One dependency, httpx. Python 3.10 or newer.
#Your first render
import uuid
from auray_client import Client
auray = Client() # reads AURAY_API_KEY
result = auray.subscribe(
"auray-ai/krea-2-turbo/text-to-image",
input={"prompt": "a brass orrery on a walnut desk, morning light", "aspect_ratio": "16:9", "resolution": 1024},
idempotency_key=str(uuid.uuid4()), # one per submit: a fixed key hands back the first job
on_queue_update=lambda s: print(s["status"]),
)
assets = auray.platform.jobs.assets(result["request_id"])
print(assets["assets"][0]["url"])Responses are plain dictionaries — the JSON the API sent, with the keys the reference documents — rather than classes that would have to be kept in step with it.
#The client
Client(credentials=None, *, retries=2, transport=None) reads AURAY_API_KEY when no key is passed;
platform=, queue=, run= and openai= (since 0.2.0) override the hosts, which you will not need. AsyncClient
takes the same arguments and has the same methods, awaited:
from auray_client import AsyncClient
async with AsyncClient() as auray:
listing = await auray.platform.models.list(status="connected")#Running a model
run(address, *, input, idempotency_key=None, wait_seconds=None, timeout=None)— the synchronous host. A504 run_timed_outis followed by the job it names,queue_request_id— never by itsrequest_id, which on an error is the trace of the call — and is never resubmitted.wait_seconds(1 to 300) is how long the host may hold the connection before it answers with the job;timeoutis the whole call's deadline in seconds. Left out, a follow waits as long as the server says the job may take —give_up_after_seconds— plus 6 minutes for the sweep that ends an overdue job, and 20 minutes until the server has said. Running out raisessubscribe_timed_outwith the job inerror.body—charged,queue_request_idand its three URLs — andfollow_target(error.body)reads it back.subscribe(address, *, input, idempotency_key=None, webhook_url=None, wait_seconds=None, priority=None, max_queue_length=None, request_timeout_seconds=None, logs=False, on_queue_update=None, timeout=None)— submit, follow, fetch the result. Withouttimeoutit waits what the submit'sgive_up_after_secondssays. (priority,wait_seconds,max_queue_length,request_timeout_secondsand the server-sized wait: since 0.2.0.)priorityandmax_queue_lengthshape a queue, and only a model that queues has one —lifecycle.queueiswaitsonGET /v1/models; a model that refuses when full, every catalogue model today, answers them422 queue_not_offeredbefore anything is charged.request_timeout_secondsis a time to start, and such a model accepts it: it starts or refuses at once.queue.submit(address, *, input, idempotency_key=None, webhook_url=None, wait_seconds=None, priority=None, max_queue_length=None, request_timeout_seconds=None),queue.status(address, *, request_id, logs=False),queue.result(address, *, request_id),queue.cancel(address, *, request_id).queue.stream_status(address, *, request_id, logs=False)— an iterator (an async iterator onAsyncClient) over the status stream, untilCOMPLETED. The server closes each stream after a few minutes; the iterator reopens it after the stream's ownretry:pause (since 0.2.1 — 0.2.0 ended there, with the job still running).queue.resultof a request that has not completed raisesrequest_in_progress.
#Files
storage.upload(*, address, content_type, data, idempotency_key=None) (address since 0.2.0; on 0.1.1 pass
product="threed" or "external" instead, which a key minted with the kinds uses for the kinds of file some
address of its kinds takes, a mesh with 3d:write) takes bytes;
storage.upload_file(path, *, address, content_type=None, idempotency_key=None) takes a path and
guesses the content type from its extension when you do not say. Both are the three calls of
Uploads for the address the file is for, and answer {"path", "bucket", "url", "expires_at", "address", "send_as"} — send_as names the field that takes the file. product is
still accepted in place of address.
#The account's side
platform.me(), platform.usage(window=None), platform.plans(), platform.catalogue(),
platform.status(), platform.changelog(), platform.jobs.{list, get, wait, cancel, assets},
platform.models.{list, document}, platform.webhooks.{list, create, verify, delete},
platform.chat.{turn, conversation, conversations, messages} — the platform reference,
one method per operation, keyword arguments named as the query and body fields are. plans,
catalogue, status and changelog need no key; chat.conversations and chat.messages need chat:read, which neither
text:write nor the older chat:write stands in for. Since 0.2.0: plans, catalogue, status, changelog, usage's window,
chat.conversations and chat.messages, and the jobs.list filters. A test in the package calls every operation
the reference names, or says in words why a program has no use for it — the support flows and MCP.
#The Community
platform.community (since 0.3.0) is the Community reference: published works, public
profiles, likes, follows, comments, collections and reports. The reads need no key — feed, search,
publications.{get, comments, remixes}, profiles.{get, publications, followers, following, collections, available}
and collections.get for a public collection — and a key that holds community:write reads as its account, its own
likes and follows filled in; any other key reads what a stranger reads. Everything that writes needs that scope, which a
developer key holds only when it was ticked as the key was made: it acts in public under the account's profile and
spends nothing.
page = auray.platform.community.feed(sort="trending", kind="image", limit=24)
work = auray.platform.community.publications.publish(
job_id="photo_8f14e45f-ceea-467f-a0e6-1d3d1c7a5b2e",
licence=True,
title="Lighthouse at dusk",
)
print(work["url"])publications.publish takes the id a submit answered and publishes that result: licence=True grants the licence
the Terms state for it (terms_version names the version you accepted), asset picks one file of a job that made
several, and show_prompt and discoverable decide what strangers see. A video, a model or a song is published with
a poster you supply — (content_type, data), PNG, JPEG or WebP, the bytes or their base64 — and a song may carry its
waveform. A work shows at once on your profile and its own link, and reaches the public feeds once its review
approves it. publications.{update, unpublish, comment, like, unlike, view, source, mine, liked},
comments.delete, profiles.{claim, update, delete, follow, unfollow, set_avatar, clear_avatar, set_banner, clear_banner}, collections.{mine, create, update, delete, add, remove}, me and report are the rest, one
method per operation, on AsyncClient as well. In update, None leaves a field as it is and CLEAR (from
auray_client) sets it to null; a profile's new handle is new_handle. report also works with no key, as an
anonymous report. Every page carries next_cursor; pass it back as cursor for the next one.
#Embeddings
embeddings.create(*, model, input, idempotency_key=None) (since 0.2.0) posts a string or a list to the
OpenAI-compatible route, POST /openai/v1/embeddings — the key needs embeddings:write:
vectors = auray.embeddings.create(model="qwen3-embedding-8b", input=["a brass trumpet", "a tuba"])
print(len(vectors["data"][0]["embedding"]))Idempotency-Key is sent only when you name one. A batch is sent again after embeddings_warming's Retry-After,
under the key you named as well: a batch refused with its whole charge given back does not use its key up, so the
identical resend is a fresh attempt, charged only if it is served, up to 8 times
for one key. A key that cannot be tried again — past those attempts, or a call a promotion paid for — is raised
with retry == "new_key" and not resent: send the batch under a new key.
#Errors
from auray_client import AurayError
try:
auray.run("auray-ai/krea-2-turbo/text-to-image", input={"prompt": "…"})
except AurayError as error:
print(error.status, error.code, error.error_type, error.message, error.docs, error.request_id)
print("retry:", error.retry, "charged:", error.charged)
if error.retryable:
print("try again in", error.retry_after_seconds, "seconds")Since 0.2.0 a refusal is resent by the client only when its own retry is same_key or after_retry_after,
as Retry-After says; everything else is raised the first time. retryable is true when trying
again can succeed without paying twice, and never while charged is stands.
run is the exception, because it submits: it is never resent. When every connection
auray.run could hold for it is in use, it raises 429 wait_capacity, refused before anything
was charged, so the same call is safe to make again after retry_after_seconds. subscribe
queues instead of holding a connection, so it never meets that refusal.
#Where it lives
packages/sdk-python in the repository. Its hosts and its version are held to the contracts
package by a guard, and its tests run the same scripted scenarios as the JavaScript client's, so
the two cannot drift apart in what they do on a 504, a 429 or a cancelled request.