squarecloud-api
The official Python SDK for the Square Cloud API.
- Zero runtime dependencies: only the standard library (
http.client,json,ssl), with one keep-alive connection per thread. - Runs on Python 3.11+, sync (
SquareCloud) and async (AsyncSquareCloud), fully typed (TypedDictresponses,py.typed). - Covers all 67 operations of the Square Cloud API, checked against the pinned OpenAPI spec on every CI run.
- Uploads and snapshot downloads stream; realtime logs and status come as an iterator (or
async for). - One error type,
SquareCloudAPIError, for every API, network and local failure.
Documentation · Releases · Migration guide
Installation
pip install squarecloud-api
uv add squarecloud-api
poetry add squarecloud-api
Requires Python 3.11 or newer.
API key
Create one at squarecloud.app/account/security. A key can be limited to scopes (apps:read, apps:deploy, ...) and to specific apps or databases. A call outside those limits raises SquareCloudAPIError with 403 MISSING_SCOPE or RESOURCE_NOT_ALLOWED, and list endpoints return only the resources the key can see. An unknown, revoked or expired key is 401 ACCESS_DENIED.
Quick start
import os
from squarecloud import SquareCloud
with SquareCloud(os.environ['SQUARECLOUD_API_KEY']) as client:
me = client.account.me()
print(me['user']['name'], [app['name'] for app in me['applications']])
app_id = me['applications'][0]['id']
client.apps.restart(app_id)
print(client.apps.logs(app_id))
The same API with await:
import asyncio
import os
from squarecloud import AsyncSquareCloud
async def main() -> None:
async with AsyncSquareCloud(os.environ['SQUARECLOUD_API_KEY']) as client:
statuses = await client.apps.status_all()
print([s['id'] for s in statuses if s['running']])
asyncio.run(main())
More in examples/: apps, realtime, snapshots, databases, workspaces, async client.
Configuration
import os
from squarecloud import BASE_URL, SquareCloud, __version__
client = SquareCloud(
os.environ['SQUARECLOUD_API_KEY'],
base_url=BASE_URL, # the options are keyword-only; these are the defaults
timeout=30.0,
max_retries=2,
transport=None,
user_agent=f'squarecloud-sdk-py/{__version__}',
)
| Option | Default | Notes |
|---|---|---|
api_key |
required | Sent raw in Authorization. An empty or whitespace-only key raises ValueError. |
base_url |
BASE_URL = https://api.squarecloud.app/v2 |
A trailing / is stripped. |
timeout |
30.0 |
Seconds per socket operation. <= 0 disables every timeout, floors included. See Retries, timeouts and rate limits. |
max_retries |
2 |
Negative counts as 0. |
transport |
HTTPTransport(timeout) |
Plug your own HTTP client (see below). |
user_agent |
squarecloud-sdk-py/<version> |
Replaces the whole User-Agent header. |
AsyncSquareCloud takes the same arguments. Both clients are thread-safe; call close() (or use with / async with) to close their connections. The version is squarecloud.__version__.
Custom transport
transport= accepts any callable transport(method, url, headers, body, timeout, stream) that returns an object with status, read(), readline() and close() (an http.client.HTTPResponse qualifies). Use it for proxies, tracing or tests. body is bytes, an iterable of bytes chunks (with Content-Length already set) or None. timeout is the per-socket-operation timeout (raised for the held calls), or None for uploads, large file writes and the realtime stream, which a custom transport should bound on connect itself. The default HTTPTransport(timeout) still bounds their connect, and asks for gzip on every call except the streamed ones (realtime and snapshot downloads).
Logging
The SDK logs each request at DEBUG on the squarecloud logger (method, path and status; never bodies or keys) and attaches only a NullHandler:
import logging
logging.basicConfig()
logging.getLogger('squarecloud').setLevel(logging.DEBUG)
API
Every app id may also be the composite '<appId>-<workspaceId>' to act on an app shared with you through a workspace. Optional modifiers are keyword-only.
| Group | Methods |
|---|---|
account |
me(), snapshots(*, scope=None) |
service |
status() (public route: the API does not check the key, but the client still needs a non-empty one) |
ai |
chat(request) (OpenAI-compatible, non-streaming) |
apps |
create(file), get(id), delete(id), status_all(*, workspace_id=None), status(id, *, raw=False), start(id), stop(id), restart(id), logs(id), metrics(id), realtime(id), domains(), load_balancers(), commit(id, file, *, path=None, filename=None) |
apps.deploys |
set_webhook(id, access_token), link_github_app(id, repository, branch), unlink_github_app(id), list(id), current(id) |
apps.envs |
get(id), set(id, envs), replace(id, envs), delete(id, keys) |
apps.files |
list(id, path=None), read(id, path) → bytes, write(id, path, content), move(id, path, to), delete(id, path) |
apps.snapshots |
list(id), create(id), restore(id, name, version_id) |
apps.network |
analytics(id, start, end, **filters), errors(id, start, end, *, include_4xx=False), logs(id, start, end), performance(id, start, end), dns(id), set_domain(id, domain), purge_cache(id) |
databases |
create(name, *, type, version, memory), get(id), update(id, *, name=None, ram=None), delete(id), start(id), stop(id), status(id, *, raw=False), metrics(id), status_all(), certificate(id), reset_credentials(id, 'password' | 'certificate') → the new password, or '' |
databases.snapshots |
list(id), create(id), restore(id, name, version_id) |
workspaces |
create(name), list(), get(id), delete(id), leave(id) |
workspaces.members |
add(workspace_id, code, group), update(workspace_id, member_id, group), remove(workspace_id, member_id), invite_code() |
workspaces.apps |
add(workspace_id, app_id), remove(workspace_id, app_id) |
| client | download_snapshot(url, dest), close() |
AsyncSquareCloud has the same groups and methods, each returning a coroutine (it runs the sync call in asyncio.to_thread), except close(), which is sync, and apps.realtime(id), which returns an AsyncRealtime to use with async for. Field names are the API's own (created_at, content_type, include_4xx...), so the API reference applies as-is. Responses are TypedDicts from squarecloud.types: plain dicts, so fields the API adds later are kept. A string result is never None ('' when the API sends nothing). start/end take an ISO 8601 string (sent as is) or a datetime (sent as UTC; a naive datetime is local time). The analytics filters are keyword-only: country, ip, path, status, os, browser, protocol, referer, provider, content_type, bot. provider takes the 'NAME (ASN)' form the providers buckets report (provider='GOOGLE (15169)'); an invalid filter is 400 INVALID_FILTER. metrics() returns up to 24 h of 5-minute points, newest first. status() formats cpu and ram as strings ('120.4MB'); with raw=True they are numbers. Empty, . and .. ids are rejected with INVALID_ID before sending, because they would reach another route. apps.network.analytics, errors and performance return None for a window with no traffic.
Usage
Uploads
apps.create and apps.commit take a path, bytes or a binary file object, and stream it: a path or a seekable file is never loaded into memory. A commit unpacks a .zip at path (the app root by default); any other file lands at path/<filename>. The filename is filename=, else the file's own name, else app.zip on create and commit.zip on commit. A zip over 100 MB fails locally with FILE_TOO_LARGE before anything is sent. Uploads have no timeout.
created = client.apps.create('bot.zip')
client.apps.commit(created['id'], 'patch.zip', path='/src')
client.apps.commit(created['id'], b'print(1)', path='/src', filename='main.py')
Files
client.apps.files.write(app_id, '/config.json', '{"debug": false}') # str or bytes
data: bytes = client.apps.files.read(app_id, '/config.json')
client.apps.files.move(app_id, '/config.json', '/config.old.json')
A str travels as text; bytes travel base64-encoded (binary-safe, about 1.33x on the wire). Empty content ('' or b'') creates an empty file. Content over 10 MB fails locally with FILE_TOO_LARGE, and content over 1 MiB is sent without a timeout, like an upload; invalid content is 400 INVALID_CONTENT. read always asks for base64 and returns the decoded bytes (at most 10 MB, else 413 FILE_TOO_LARGE). list of a missing directory is 404 FILE_NOT_FOUND, and a blocked path is 403 BLOCKED_PATH. Paths are at most 256 characters.
Snapshots
create returns {'pending': True} while the API is still generating the snapshot (HTTP 202 SNAPSHOT_PROCESSING). It then appears in list on its own, usually within 2 minutes: poll list, and never call create again, which is limited to one per 180 seconds and counts against the plan's daily snapshot quota.
snapshot = client.apps.snapshots.create(app_id)
if not snapshot['pending']:
client.download_snapshot(snapshot['url'], 'backups/') # -> backups/<name>.zip
latest = max(client.apps.snapshots.list(app_id), key=lambda s: s['modified'])
client.apps.snapshots.restore(app_id, latest['name'], latest['version_id'])
client.download_snapshot(latest['url'], 'backups/')
Each listed item carries the API's version_id (what restore needs) and a signed download url, passed through as sent. download_snapshot never sends the API key to the snapshot host, streams to a .part file next to the target and renames it when complete, and bounds each read with timeout.
Realtime
with client.apps.realtime(app_id) as stream:
for event in stream:
if event['event'] == 'logs':
print(event['stream'], event['line']) # stdout | stderr
elif event['event'] == 'status':
print(event['status'].get('cpu')) # always the full, merged state
else: # 'system' or 'error'
print(event['data']) # a code such as REALTIME_DISCONNECTED
Every event has event, data (the raw frame text) and id. The HTTP status is checked before streaming, so 429 REALTIME_MAX_CONNECTIONS raises. A dropped connection, or the API's REALTIME_RECONNECT hand-off, reopens up to 3 times in a row (a logs or status event resets the count), at most one open per 5.5 s to stay under the API's pace of one per 5 s; past that it raises NETWORK_ERROR. The open is timed until the response headers arrive; the stream itself is not. The loop ends on a clean close (10-minute server limit), on REALTIME_DISCONNECTED, or on stream.close() (safe from any thread, also mid-wait). With AsyncSquareCloud: async with client.apps.realtime(app_id) as stream: async for event in stream: .... Max 5 concurrent streams per account and 30 per app.
GitHub deploys
# A GitHub webhook: returns its URL ('' when removed with '@')
url = client.apps.deploys.set_webhook(app_id, 'ghp_xxx')
# Or the Square Cloud GitHub App
repo = client.apps.deploys.link_github_app(app_id, 'octocat/hello-world', 'main')
print(repo['id'], repo['full_name'], repo['branch'])
current = client.apps.deploys.current(app_id) # {} when nothing is set
client.apps.deploys.unlink_github_app(app_id)
Linking needs scope apps:deploy and a GitHub App installation on your account (403 GITHUB_NOT_CONNECTED otherwise). The repository must belong to a GitHub App installation your connected GitHub account holds (403 REPOSITORY_NOT_AVAILABLE otherwise), and that account needs write access to it (403 REPOSITORY_PERMISSION_REQUIRED). 502 FAILED_TO_FETCH means GitHub did not confirm the branch; it is safe to retry. A repository and branch can be linked to one app across all accounts (409 REPOSITORY_BRANCH_ALREADY_CONFIGURED; the other app's id appears only in message, and only when that app is yours). Re-linking needs an unlink first (400 GIT_ALREADY_CONFIGURED), and unlinking without a link is 400 GIT_NOT_CONFIGURED. Link and unlink share a limit of 3 calls per 60 s.
Errors
Every API and network failure raises SquareCloudAPIError with status, code, message, method, path ('/v2/apps/...', never the query string) and cause. Local file problems, such as a missing upload path or an unwritable download destination, raise OSError.
from squarecloud import SquareCloudAPIError
try:
client.apps.start(app_id)
except SquareCloudAPIError as e:
print(e) # POST /v2/apps/<id>/start: HTTP 404 APP_NOT_FOUND
if e.code == 'MISSING_SCOPE':
...
- No response:
status0withcodeNETWORK_ERROR(original exception incause) orTIMEOUT. - Local checks, nothing sent:
status0withFILE_TOO_LARGE, orINVALID_IDfor an id that is empty,.or... messageis the server's explanation, or''when it sent only a code. A body without a code (a proxy page, a failed snapshot download) isUNKNOWN_ERROR, with the server's message when it sent one andHTTP <status>otherwise; a 2xx body that is not JSON isUNKNOWN_ERRORwithInvalid JSON in HTTP <status> response.- Start, stop and restart refusals are 409 with only a code (
messageis''):CONTAINER_ALREADY_STARTED,CONTAINER_ALREADY_STOPPED,CONTAINER_TEMPORARILY_SUSPENDED(apps),CONTAINER_NOT_FOUND,CONTAINER_INSUFFICIENT_DISK_SPACE,CONTAINER_NETWORK_CONFLICTorACTION_FAILED. They are errors: checke.codeif an already started or stopped resource is fine for you. A 2xx reply whose body is{"status": "error"}also raises; a 202SNAPSHOT_PROCESSINGstays{'pending': True}. ai.chat()errors, auth, 429 and 503 included, carry the OpenAI error's lowercasecode(access_denied,upgrade_required,rate_limit_exceeded,server_overloaded,database_unavailable, ...), or itstypewhen there is no code.- The
SquareCloudAPIErrordocstring lists every known API code. The list grows: treat an unknown code as a generic failure of its HTTP status. str(e)is<METHOD> <path>: HTTP <status> <CODE>: <message>, withoutHTTP <status>when the status is0and without: <message>when it is empty.
Retries, timeouts and rate limits
- Timeouts:
timeout(30 s) applies per socket operation. Calls the server holds open wait at least 120 s (start/stop/restartof apps and databases,databases.create, snapshotcreate/restore), and so doesai.chat(), whose gateway has one 90 s deadline for the whole request and answers 503server_overloadedpast it; a largertimeoutwins.timeout <= 0disables every timeout, floors included. Uploads andfiles.writeabove 1 MiB have no timeout;realtime()is timed only until it opens. - Retries: the SDK retries only what is safe: network errors on
GET(including a body cut off mid-read, realtime opens anddownload_snapshot()before the response), and 503UPLOAD_BUSY/ANALYTICS_BUSY(plusDATABASE_UNAVAILABLEonGET), up tomax_retries(2) times with exponential backoff (500 ms·2ⁿ, jitter, max 8 s). Timeouts, 429 and the AI's 503server_overloaded(safe for you to retry) are never retried.DATABASE_UNAVAILABLEis not retried on other methods because it can fire after a mutation was applied; retrying an idempotent one (afiles.write, anenvs.replace, astop) is up to you. - Rate limits: every account has a global limit of requests per 60 s, set by its plan (values). Going over it, or over a route's own limit, returns 429
RATE_LIMITEDorKEEP_CALM.RATE_LIMITEDcan block the account, API key or IP for about 30 minutes; it is also the limit of the network endpoints andaccount.snapshots().RATE_LIMITandRATE_LIMIT_EXCEEDEDare its deprecated names. The API sends noRetry-After, which is why the SDK never retries a 429.
Development
uv sync
uv run ruff format --check && uv run ruff check && uv run mypy && uv run pytest # offline
uv build
The offline suites (tests/test_transport.py, tests/test_http.py, tests/test_streams.py, tests/test_conformance.py) use a mock transport or a loopback server and never reach the API. The live suite runs all 67 operations against the real API:
SQUARECLOUD_API_KEY=... uv run pytest -m live -s
Warning: the live suite creates and deletes real resources (an app, a database and a workspace named
sdk-live-py-<timestamp>) on the key's account. It is skipped withoutSQUARECLOUD_API_KEYand never runs in CI. Requests are spaced 2.1 s apart; run the JS, Python and Go live suites one after another, never at the same time.
Contributing
Issues and pull requests are welcome at squarecloudofc/sdk-api-py.
License
MIT, see LICENSE.
Authors
Maintained by Square Cloud.
Contributors:
- Robert Nogueira (@robert-nogueira)
- Jhonatan Jeferson (@Jhonatan-Jeferson)
Metadata
Release files for squarecloud-api 5.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| squarecloud_api-5.0.0.tar.gz | 29.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| squarecloud_api-5.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 61.4 kB
Release files / squarecloud_api-5.0.0.tar.gz
| Download URL | squarecloud_api-5.0.0.tar.gz |
|---|---|
| Size | 29.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
47afa161b340e3e7024cadbe3d6bee018281c3197fce61ae4354838e692350d2
|
|
BLAKE2b-256 checksum How to use checksums |
59de52a7c0369ac8c06814d4201af2567ea3f7a1bb265eb14bfb674c0b1b2404
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / squarecloud_api-5.0.0-py3-none-any.whl
| Download URL | squarecloud_api-5.0.0-py3-none-any.whl |
|---|---|
| Size | 31.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
544e58c19ded8c5308433b4d5ef4ee393019c95c95ff803f80ecaf44c66d168a
|
|
BLAKE2b-256 checksum How to use checksums |
8b537a351b9df483afa881683062f61b39cc4fd4fca2a0d5ee5772531cce5296
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|