picx-ai
Official Python client for the PicX image and video generation API.
- Sync (
PicX) and async (AsyncPicX) clients with the same surface - Typed results and a typed exception hierarchy; ships
py.typed, passesmypy --strict - Automatic retries with exponential backoff + jitter,
Retry-Afteraware Idempotency-Keysupport on the calls that spend credits- One runtime dependency:
httpx
This is not the CLI. This package is a library you install into your own application. The
admin-cli/project in this organisation builds the internalpicx-admincommand line binary and is unrelated to this SDK.
Install
pip install picx-ai
# or
uv add picx-ai
Requires Python 3.9+.
Quickstart
import os
from picx import PicX
picx = PicX(os.environ["PICX_API_KEY"])
job = picx.video.create(prompt="sneaker on marble, slow orbit", duration=12)
asset = job.wait()
print(asset.url)
PicX() falls back to the PICX_API_KEY environment variable when no key is
passed, so PicX() works once the variable is set. Use it as a context manager
to close the connection pool deterministically:
with PicX() as picx:
asset = picx.images.generate("a red sneaker on white marble", size="2K", aspect_ratio="1:1")
print(asset.url, asset.credits_used)
Configuration
picx = PicX(
api_key="pxsk_...", # defaults to os.environ["PICX_API_KEY"]
base_url="https://api.picxstudio.com/v1", # or the PICX_BASE_URL env var
timeout=60.0, # seconds, per request
max_retries=2, # retries for 429 / 5xx / connection errors
)
API
| Call | Endpoint | Notes |
|---|---|---|
picx.images.generate(prompt, model=…, size=…, aspect_ratio=…) |
POST /images/generate |
scope images:generate; size is 1K/2K/4K, aspect_ratio like 16:9 |
picx.images.edit(instruction, image_urls, model=…, size=…) |
POST /images/edit |
scope images:edit; 1-5 image URLs |
picx.video.create(prompt=…, duration=…, resolution=…, sound=…, …) |
POST /videos/generate |
scope videos:generate; returns 202 and a job to poll |
picx.generations.get(id) |
GET /generations/{id} |
raises NotFoundError on 404 |
picx.models.list(type="image") |
GET /models |
public, no API key needed |
picx.account.usage(period=30) |
GET /account/usage |
|
picx.account.me() |
GET /account/me |
picx.images is also available as picx.image, and picx.video as picx.videos.
Images
asset = picx.images.generate("a red sneaker on white marble", size="2K", aspect_ratio="1:1")
asset.id, asset.url, asset.model, asset.size, asset.aspect_ratio, asset.credits_used
edited = picx.images.edit("put it on a wet street at night", [asset.url], size="4K")
Managed uploads (assets)
images.edit and the video modes need public URLs — they can't read a local file. Upload it first:
asset = picx.assets.create("portrait.jpg", metadata={"source": "onboarding"})
print(asset.id, asset.url)
# Now use the URL in an edit or video call:
avatar = picx.images.edit(
"turn this into a naive doodle avatar",
image_urls=[asset.url],
)
file accepts a path (string or Path), an open binary file, or raw bytes:
asset = picx.assets.create(b"\x89PNG...", filename="photo.png", content_type="image/png")
List, get, and delete:
result = picx.assets.list(kind="image", limit=50)
for a in result:
print(a.id, a.url)
single = picx.assets.get(asset.id)
picx.assets.delete(asset.id) # soft delete — existing URLs keep working
The async client has the same surface:
async with AsyncPicX() as picx:
asset = await picx.assets.create("portrait.jpg")
Videos (202 Accepted, then poll)
POST /videos/generate is asynchronous server-side, so create() returns a job:
job = picx.video.create(
prompt="sneaker on marble, slow orbit",
duration=12, # server default 5
resolution="720p", # server default "720p"; must be priced for the model
sound=True, # server default True
aspect_ratio="16:9",
mode="image", # "text" | "image" | "reference"
image_url="https://…/ref.png",
callback_url="https://example.com/webhook",
)
job.id, job.status # "gen_…", "queued"
generation = job.wait(timeout=900, poll_interval=5)
print(generation.url) # alias for .output_url
wait() polls GET /generations/{id} until a terminal status
(succeeded, completed, failed, error, cancelled, canceled).
It raises JobFailedError on a failed generation — pass
raise_on_failure=False to get the Generation back instead — and
JobTimeoutError if the timeout elapses first. job.refresh() polls once.
Anything left unset is omitted from the request body so the server applies its
own defaults (model fal-ai/bytedance/seedance/v2, duration 5, resolution
720p, sound on).
Models, usage, account
for model in picx.models.list(type="video"):
print(model.id, model.name, model.credits)
usage = picx.account.usage(period=30)
print(usage.total_requests, usage.credits_used, usage.total_cost_usd, usage.model_breakdown)
me = picx.account.me()
print(me.email, me.is_active, me.credits)
GET /models is the only public endpoint, so PicX(api_key=None).models.list()
works without credentials.
Every result object also keeps the untouched response on .raw, so new API
fields are reachable before the SDK models them.
Async
import asyncio, os
from picx import AsyncPicX
async def main() -> None:
async with AsyncPicX(os.environ["PICX_API_KEY"]) as picx:
job = await picx.video.create(prompt="sneaker on marble, slow orbit", duration=12)
asset = await job.wait()
print(asset.url)
image = await picx.images.generate("a red sneaker on white marble")
print(image.url)
asyncio.run(main())
The async surface mirrors the sync one method for method — only await and
aclose()/async with differ. A parity test enforces this.
Error handling
from picx import (
PicXError, # base class: .status_code, .request_id, .body, .message
ValidationError, # 400 / 422, and invalid arguments caught locally
AuthenticationError, # 401, or no API key configured
PermissionDeniedError, # 403, the key lacks the required scope
NotFoundError, # 404
RateLimitError, # 429, exposes .retry_after
ServerError, # 5xx
APIConnectionError, # DNS/TLS/socket failure
APITimeoutError, # subclass of APIConnectionError
JobFailedError, # a generation ended failed/cancelled
JobTimeoutError, # wait() timed out, generation still running
)
try:
asset = picx.images.generate("a red sneaker")
except RateLimitError as exc:
print("slow down for", exc.retry_after, "seconds")
except PermissionDeniedError:
print("this key is missing the images:generate scope")
except PicXError as exc:
print(exc.status_code, exc.request_id, exc)
Errors are parsed from both response shapes the API uses — FastAPI's
{"detail": …} (including the 422 list form) and {"error": …, "detail": …}.
Retries and idempotency
Retries apply to 429, 5xx and connection/timeout failures only — never to
other 4xx. Backoff is exponential with full jitter and honours Retry-After
(capped at 60s). max_retries defaults to 2; set max_retries=0 to disable.
A POST is not replayed unless you supply an idempotency key, because
replaying it could charge twice:
import uuid
asset = picx.images.generate("a red sneaker", idempotency_key=str(uuid.uuid4()))
The key is sent as the Idempotency-Key header, which the backend honours on
calls that spend credits. idempotency_key is available on
images.generate, images.edit and video.create.
API keys are never logged
The key is redacted from repr()/str() of the client and from every exception
message and response body (anything matching pxsk_… becomes
pxsk_***REDACTED***). Read it back deliberately with picx.api_key if you
really need it. This is covered by tests.
Examples
export PICX_API_KEY=pxsk_...
python examples/generate_image.py "a red sneaker on white marble"
python examples/generate_video.py "sneaker on marble, slow orbit"
python examples/generate_video.py --async
Development
uv venv --python 3.13
uv pip install -e ".[dev]"
uv run mypy
uv run pytest
The test suite runs entirely against httpx.MockTransport — no network access,
no API key required.
License
MIT
Metadata
Release files for picx-ai 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| picx_ai-0.3.1.tar.gz | 39.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| picx_ai-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 73.4 kB
Release files / picx_ai-0.3.1.tar.gz
| Download URL | picx_ai-0.3.1.tar.gz |
|---|---|
| Size | 39.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
80c2f0cb075c33c20aac819364db7bba9b1edb8a50bf04cefadaa9cd63e056ee
|
|
BLAKE2b-256 checksum How to use checksums |
e06ffeff49718dffd2314c690326cbb11086c878cbb2726e70c9835f8ea6ad2c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 25, 2026.
Transparency logRelease files / picx_ai-0.3.1-py3-none-any.whl
| Download URL | picx_ai-0.3.1-py3-none-any.whl |
|---|---|
| Size | 34.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a3528fe1dcaf9336225d0aeccf4a9c9e50db94f9f6e484a13e865a5dcca4efa0
|
|
BLAKE2b-256 checksum How to use checksums |
cce051ffd06f7370832d298bee22e8a8f2258fc4042e5b0e3a9489cc713aa8fa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 25, 2026.
Transparency log