API
Python SDK
The reuseforge Python package, standard library only: one method per operation, auto-paginating lists, retries, typed errors, safe installs, webhook verification and a CLI.
reuseforge is a client for the whole API: one method per operation, the JSON the server sent, no dependencies beyond the Python 3.9+ standard library, and type hints (py.typed). It behaves exactly like the JavaScript SDK; both are tested against the same examples from the contract.
Not on PyPI yet. Install it from a checkout of the repository: pip install ./sdk/python.
git clone https://github.com/Longfordjames/Claude-1 && cd Claude-1
pip install ./sdk/python
python3 -c "import reuseforge; print(reuseforge.__version__)"First calls
import reuseforge
client = reuseforge.Client() # token from REUSEFORGE_TOKEN, else the file `rf login` wrote, else anonymous
for item in client.search_items(q="invoice pdf", kind=["skill", "harness"], max_price_cents=2000): # auto-paginates
print(item["full_name"], reuseforge.dollars(item["price_cents"]))
plan = client.plan_goal(goal="RAG chatbot over invoices", budget_cents=5000, online=True)
print(plan["recommended"]["paid_total_cents"], "cents to buy,", len(plan["pareto_front"]), "options")
result = client.install("acme/invoice-extractor", target="claude-code", dest=".") # never pays, never runs anything
print(result["version"], [f["path"] for f in result["files"]])Calling conventions
- Names. One method per
operationId, verbatim:client.search_items,client.get_install_descriptor,client.create_order. No namespaces. The reference shows the exact call for every operation. - Arguments. Path parameters are positional in path order, except that
ownerandslugcollapse into oneitemargument:"owner/slug", or an item object (itsfull_nameis used). Query parameters and body fields are keyword arguments. Lists for comma-separated query parameters (kind,capability,license) are joined for you. - Results. Plain
dicts with the wire keys, exactly what curl prints. Nothing is renamed or converted.TypedDicts for every schema live inreuseforge.modelsfor editors and type checkers. - Money. Integer cents.
reuseforge.dollars(1200) == "12.00"andreuseforge.cents("12.00") == 1200exist for people;centsparses strings andDecimal, never floats.
Configuration
Resolved highest first: arguments, environment (REUSEFORGE_TOKEN, REUSEFORGE_API_URL, REUSEFORGE_PROFILE), ./.reuseforge/config.json (never tokens), ~/.config/reuseforge/config.json, then credentials.json next to it (mode 0600; a file other users can read is refused), then anonymous against https://reuseforge.com/api/v1. REUSEFORGE_CONFIG_DIR moves the directory. The CLI and both SDKs share all of it.
client = reuseforge.Client(token="rf_...", base_url="https://reuseforge.com/api/v1", timeout=30.0, max_retries=2)
fast = client.with_options(timeout=5, max_retries=0) # a clone with different defaults
client.last_response.request_id, client.last_response.rate_limit, client.last_response.etagThe token is never logged, never in an exception message, and repr(client) shows rf_...abcd. It is not sent over plain http except to loopback addresses. Timeouts are per request: 30 seconds, and 60 for plan_goal, discover_external and pay_order.
Pagination
List methods return a Page: .data, .next_cursor, .has_more. Iterating it fetches further pages lazily, page.pages() iterates page objects, and max_items= stops early. See pagination.
page = client.search_items(q="pdf", limit=100)
print(len(page.data), page.has_more)
for item in client.search_items(q="pdf", max_items=500):
...ETags, retries, idempotency
item = client.get_item("acme/invoice-extractor")
same = client.get_item("acme/invoice-extractor", etag=client.last_response.etag) # None on 304
client.update_item("acme/invoice-extractor", {"summary": "New summary"}, if_match="auto") # reads the ETag first- Retried up to
max_retries(default 2): network errors and timeouts,429,502,503,504, and500for GET, PUT, DELETE and for a POST that carries anIdempotency-Key. Other 4xx are never retried. - Backoff is
min(8 s, 0.5 s * 2^attempt)with full jitter.Retry-Afterwins up to 60 seconds; a longer one raises immediately. - Every operation that declares
Idempotency-Keygets a fresh UUID that is reused across the retries of that call. Passidempotency_key="..."to control it. See idempotency.
Errors
All failures derive from ReuseForgeError with status, code, title, detail, request_id, errors, problem (the raw body) and headers.
| Class | When |
|---|---|
AuthenticationError | 401 |
ForbiddenError | 403, with required_scopes; SpendLimitError for spend_limit_exceeded |
PaymentRequiredError | 402, with order_hint |
NotFoundError | 404, 410 |
ConflictError | 409, 412, 428 (problem["current_etag"]) |
ValidationError | 400, 413, 415, 422 |
RateLimitError | 429, with retry_after |
ServerError | 5xx, including a proxy's HTML error page (code internal) |
NetworkError | no response |
IntegrityError, UnsafePathError | a hash did not match, or an install path escaped the root (client side) |
SignatureError | webhook verification failed |
try:
client.pay_order("ord_tqjv5mw3k2na")
except reuseforge.SpendLimitError as e:
print("token may spend", e.problem["limit_cents"], "and has spent", e.problem["spent_cents"])
except reuseforge.RateLimitError as e:
print("retry in", e.retry_after)See errors for every code.
Installing safely
client.install(item, target, dest) fetches the install descriptor and the bundle, then:
- verifies every file's
sha256and the bundlecontent_hashbefore writing anything; - refuses path traversal, absolute paths, symlink escapes and oversize bundles (100 files, 512 KiB each, 2 MiB per version);
- never overwrites a file that differs from
reuseforge.lockunlessforce=True; - writes atomically (rolling back on failure), strips setuid and setgid, and caps modes at
0755; - merges JSON, TOML and Markdown config patches without clobbering your keys, and records everything in
reuseforge.lock.
It never runs a package's commands and never pays: a paid item you do not own raises PaymentRequiredError whose order_hint is the order to create.
reuseforge.content_hash({"SKILL.md": b"...", "src/run.py": b"..."}) # "sha256:..."
reuseforge.verify_bundle(bundle) # IntegrityError on any mismatch
lock = reuseforge.Lockfile.read("."); lock.verify(".") # offline drift check of installed files
manifest = reuseforge.read_manifest(".") # reuseforge.json, schema-checkedBuying
plan = client.plan_goal(goal="RAG chatbot over invoices", budget_cents=5000)
order = client.create_order(plan_id=plan["id"], rail="simulated") # Idempotency-Key generated for you
order = client.pay_order(order["id"])
order = client.wait_for_order(order["id"], states=("funded", "settled")) # polls with backoff; WaitTimeoutError on timeoutAccepting delivery (accept_order) needs the orders:accept scope; see authentication. The orders operations are not enabled on reuseforge.com yet (see the availability table).
Webhooks
event = reuseforge.webhooks.verify(raw_body, headers["ReuseForge-Signature"], secret) # tolerance 300 s, rotation-safePass the raw body bytes. Details, retries and a receiver: webhooks.
The reuseforge command
The package also installs a command line, reuseforge (and rf), with the same flags, exit codes and files as the Node CLI. It covers login, search, plan, install, update, list, verify, uninstall, init, publish, orders, doctor and more; everything else is available as SDK methods. The full command set, generated from the real --help, is on the CLI reference page.
reuseforge login --with-token < token.txt
reuseforge search "invoice pdf" --kind skill --max-price 20 --verified
reuseforge install acme/invoice-extractor@^1.2 --target claude-codeDevelopment
cd sdk/python
python3 -m unittest discover -s tests # offline: a stub transport serves the contract's own examples
python3 tools/gen_models.py --check # models.py and the operation table match api/openapi.jsonThe offline suite answers every operation with the first 2xx example from the contract, so the OpenAPI examples are the shared oracle for both SDKs and the Worker.
Last updated: