> ## Documentation Index
> Fetch the complete documentation index at: https://docs.squarecloud.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK: quickstart and client

> Install the Square Cloud Python SDK (squarecloud-api 5), create a sync or async client with your API key and follow its conventions: ids, dates and timeouts.

<Info>
  This page documents **`squarecloud-api` 5.0**, a rewrite of the SDK. Coming from v4? Read the [v4 → v5 migration guide](/en/sdks/py/migrating_to_v5).
</Info>

## Requirements

* **Python 3.11** or newer.
* An API key (see [API key and scopes](#api-key-and-scopes)).

The package has **no runtime dependencies** (only the standard library: `http.client`, `json`, `ssl`) and is fully typed (`py.typed`): responses are `TypedDict`s your editor and type checker understand.

## Installation

<Tabs>
  <Tab title="pip">
    ```bash theme={"system"}
    pip install squarecloud-api
    ```
  </Tab>

  <Tab title="uv">
    ```bash theme={"system"}
    uv add squarecloud-api
    ```
  </Tab>

  <Tab title="poetry">
    ```bash theme={"system"}
    poetry add squarecloud-api
    ```
  </Tab>
</Tabs>

The package installs as `squarecloud-api` and is imported as `squarecloud`. The installed version is `squarecloud.__version__`.

## API key and scopes

Create a key at [squarecloud.app/account/security](https://squarecloud.app/en/account/security). The SDK sends it as-is in the `Authorization` header (no `Bearer` prefix).

A key can be limited to **scopes** (`apps:read`, `apps:deploy`, `apps:control`, `ai:chat`, ...) and to specific apps or databases:

* A call outside those limits raises a [`SquareCloudAPIError`](/en/sdks/py/errors) with 403 `MISSING_SCOPE` or `RESOURCE_NOT_ALLOWED`.
* List methods (`account.me()`, `apps.status_all()`, ...) only return the resources the key can see.
* An unknown, revoked or expired key is 401 `ACCESS_DENIED`.

<Warning>
  Keep the key out of your source code: read it from the environment (`os.environ["SQUARECLOUD_API_KEY"]`) or a secret manager.
</Warning>

The examples read the key from the `SQUARECLOUD_API_KEY` environment variable. Set it in the terminal where you run them:

<Tabs>
  <Tab title="macOS / Linux">
    ```bash theme={"system"}
    export SQUARECLOUD_API_KEY="your-api-key"
    ```
  </Tab>

  <Tab title="Windows (PowerShell)">
    ```powershell theme={"system"}
    $env:SQUARECLOUD_API_KEY = "your-api-key"
    ```
  </Tab>
</Tabs>

## Creating the client

The SDK has two clients with the same groups and methods:

* **`SquareCloud`** is synchronous and **thread-safe**: share one instance between threads. Each thread reuses its own keep-alive connection.
* **`AsyncSquareCloud`** is the same API with `await`. Each call runs the synchronous client in `asyncio.to_thread`, so the event loop is never blocked.

<Tabs>
  <Tab title="Sync">
    ```python theme={"system"}
    import os

    from squarecloud import SquareCloud

    with SquareCloud(os.environ["SQUARECLOUD_API_KEY"]) as client:
        me = client.account.me()
        print(f"Hi {me['user']['name']}, you have {len(me['applications'])} apps")
    ```
  </Tab>

  <Tab title="Async">
    ```python theme={"system"}
    import asyncio
    import os

    from squarecloud import AsyncSquareCloud


    async def main() -> None:
        async with AsyncSquareCloud(os.environ["SQUARECLOUD_API_KEY"]) as client:
            me = await client.account.me()
            print(f"Hi {me['user']['name']}, you have {len(me['applications'])} apps")


    asyncio.run(main())
    ```
  </Tab>
</Tabs>

Both print your account name and the number of apps the key can see:

```text theme={"system"}
Hi Jane, you have 3 apps
```

Leaving the `with` / `async with` block closes the pooled connections. Without a block, call `client.close()` when you are done. `close()` is a regular (synchronous) method on both clients.

An empty or whitespace-only key raises a `ValueError` in the constructor, before any request.

### The async client

`AsyncSquareCloud` differs from `SquareCloud` in only three places:

* Every method returns a coroutine: `await client.apps.status(app_id)`.
* `close()` is synchronous: call it without `await`.
* `apps.realtime(app_id)` is **not** awaited: it returns an `AsyncRealtime` that you consume with `async for` (see [Realtime](/en/sdks/py/realtime)).

Since each call runs in a worker thread, you can run calls concurrently with `asyncio.gather`:

```python theme={"system"}
apps, databases = await asyncio.gather(
    client.apps.status_all(),
    client.databases.status_all(),
)
```

<Note>
  Cancelling an awaiting task does not stop the request already running in its worker thread: the call still completes (or times out) in the background.
</Note>

### Options

```python theme={"system"}
import os

from squarecloud import BASE_URL, SquareCloud

client = SquareCloud(
    os.environ["SQUARECLOUD_API_KEY"],
    base_url=BASE_URL,  # "https://api.squarecloud.app/v2" (default)
    timeout=30.0,
    max_retries=2,
    user_agent="my-bot/1.0",
    transport=None,  # a custom transport (proxy, tracing, tests)
)
```

The options are keyword-only, and `AsyncSquareCloud` takes the same ones.

| Option | Default | Description |
| - | - | - |
| `base_url` | `https://api.squarecloud.app/v2` | API base URL. A trailing `/` is stripped. Also exported as `BASE_URL`. |
| `timeout` | `30.0` | Seconds **per socket operation** (the connect and each read or write), not for the whole request. See [Timeouts](#timeouts). |
| `max_retries` | `2` | Retries of safe failures only (up to 3 attempts). A negative value counts as `0`. See [Retries](/en/sdks/py/errors#retries). |
| `user_agent` | `squarecloud-sdk-py/<version>` | Replaces the whole `User-Agent` header. |
| `transport` | `HTTPTransport(timeout)` | A custom transport for proxies, tracing or tests. See [Advanced](#advanced). |

<Note>
  The client keeps the key only in its private request headers: there is no `client.api_key` attribute, and the SDK's logger never writes it.
</Note>

## Modules

| Attribute | Covers | Page |
| - | - | - |
| `client.account` | `me()`, `snapshots(scope=)` | [Account](#account) and [Snapshots](/en/sdks/py/snapshots) |
| `client.service` | `status()` | [Platform status](#platform-status) |
| `client.apps` | Get, delete, status, lifecycle, logs, metrics, domains, load balancers | [Managing applications](/en/sdks/py/managing_application) |
| `client.apps.create` / `commit` | Uploads | [Commit and upload](/en/sdks/py/commit_and_upload) |
| `client.apps.files` | File manager | [Managing files](/en/sdks/py/managing_files) |
| `client.apps.envs` | Environment variables | [Environment variables](/en/sdks/py/environment_variables) |
| `client.apps.deploys` | Webhook and GitHub App deploys | [Deploys](/en/sdks/py/deploys) |
| `client.apps.network` | Analytics, logs, DNS, custom domain, cache | [Network](/en/sdks/py/network) |
| `client.apps.realtime` | Live logs and status | [Realtime](/en/sdks/py/realtime) |
| `client.apps.snapshots` / `client.databases.snapshots` / `client.download_snapshot` | Backups | [Snapshots](/en/sdks/py/snapshots) |
| `client.databases` | Managed databases | [Databases](/en/sdks/py/databases) |
| `client.workspaces` | Workspaces, members and shared apps | [Workspaces](/en/sdks/py/workspaces) |
| `client.ai` | `chat()` | [AI](/en/sdks/py/ai) |

The package exports `SquareCloud`, `AsyncSquareCloud`, `SquareCloudAPIError`, `BASE_URL`, `HTTPTransport`, the `Transport` and `Response` protocols, `Realtime`, `AsyncRealtime` and `__version__`. The response types (`App`, `RuntimeStats`, `Snapshot`, ...) live in `squarecloud.types`:

```python theme={"system"}
from squarecloud.types import App, Snapshot
```

## Conventions

### Ids first, plain data back

Every method takes the resource id as its first argument and returns **plain data**: each response is a `TypedDict`, which is a regular `dict` at runtime (no classes, no cache), so fields the API adds later are kept. Field names are the API's own (`created_at`, `version_id`, `lastModified`, `joinedAt`, `netIO`, ...), so the [API reference](/en/api-reference/authentication) applies as-is.

* Mutations return `None`, unless the API returns data (`envs.*`, `deploys.set_webhook`, `deploys.link_github_app`, `databases.reset_credentials` and the `create` methods).
* A string result is never `None`: it is `''` when the API sends none.
* Lists come complete in one call: there is no pagination.
* Optional modifiers are **keyword-only** (`status(app_id, raw=True)`, `commit(app_id, file, path="/src")`). The one exception is the optional `path` of `files.list`, which can also be passed by position.

Methods are regular bound methods, so you can keep a reference to them:

```python theme={"system"}
restart = client.apps.restart
logs = client.apps.logs

restart(app_id)
print(logs(app_id))
```

### Workspace apps

Every `app_id` also accepts the composite form `<appId>-<workspaceId>` to act on an app shared with you through a [workspace](/en/sdks/py/workspaces). `workspaces.get()` and `workspaces.list()` return raw ids; you build the composite id yourself:

```python theme={"system"}
client.apps.restart(f"{app_id}-{workspace_id}")
```

### Ids are encoded

Ids in the URL path are percent-encoded. An id that is empty, `.` or `..` would reach a different route, so it fails locally with `INVALID_ID` (status `0`) before anything is sent. Workspace routes send their ids in the body instead: there, `INVALID_ID` (400) comes from the server.

### Dates

`start` and `end` arguments (see [Network](/en/sdks/py/network)) take an ISO 8601 string (sent as is) or a `datetime` (sent in UTC). A **naive** `datetime` is treated as local time and converted to UTC, so prefer aware ones (`datetime.now(UTC)`). Dates in responses stay as the API sends them (ISO strings, or Unix milliseconds where the API uses them).

## Timeouts

`timeout` bounds **each socket operation**: the connect and every read or write. A response that keeps sending data can take longer than `timeout` in total.

| Call | Timeout |
| - | - |
| Every other call | `timeout` (30 s) per socket operation |
| Calls the server holds open: `start`/`stop`/`restart` of apps, `start`/`stop` of databases, `databases.create`, snapshot `create`/`restore`, `ai.chat` | At least **120 s** (a larger `timeout` wins) |
| Uploads (`apps.create`, `apps.commit`) and `files.write` above 1 MiB | None once connected (the connect is still bounded by `timeout`) |
| `download_snapshot` | `timeout` per read, not for the whole download |
| `apps.realtime` | Only until the response headers arrive |

A `timeout` of `0` or less disables **every** timeout, the 120 s floors included.

Calls cannot be cancelled once sent: the only stream you can stop midway is [`apps.realtime()`](/en/sdks/py/realtime), with `close()` from any thread. For a long upload, keep the default timeout so a dead connection is detected on connect, and run it in a thread or with `AsyncSquareCloud` if the rest of your program must keep going.

## Account

`client.account.me()` returns the authenticated user plus the apps and databases the key can see.

```python theme={"system"}
from datetime import datetime

me = client.account.me()
user = me["user"]

print(user["id"], user["name"], user["email"], user["plan"]["name"])
print(user["plan"]["memory"])  # {'limit', 'available', 'used'} in MB

duration = user["plan"]["duration"]
print(datetime.fromtimestamp(duration / 1000) if duration else "never expires")
```

| Field | Description |
| - | - |
| `user` | `id`, `name`, `email`, `locale`, `created_at` and `plan` (`name`, `memory`, `duration`: the Unix ms at which the plan **expires**, `None` if it never does) |
| `applications` | List of `{ id, name, desc?, ram, lang, domain?, custom?, cluster, created_at }` |
| `databases` | List of `{ id, name, ram, type, cluster, created_at }` |

`client.account.snapshots(scope=...)` lists every snapshot of the account: see [Snapshots](/en/sdks/py/snapshots#account-snapshots).

## Platform status

`client.service.status()` returns the public platform status. The route needs no valid key, but the client still requires a non-empty one.

```python theme={"system"}
status = client.service.status()

print(status["status"])  # "online" | "degraded" | "unknown"
print(status["message"])

for name, service in status.get("services", {}).items():
    print(name, service["status"])  # "operational" | "degraded" | "outage" | "maintenance" | "unknown"
```

`unknown` means the check itself could not run: it is not evidence of an outage.

## Advanced

### Custom transport

`transport=` replaces the HTTP layer. Use it for proxies, tracing or tests. A transport is any callable with this signature:

```python theme={"system"}
transport(method, url, headers, body, timeout, stream) -> Response
```

| Argument | Description |
| - | - |
| `method`, `url`, `headers` | The request. `headers` already carries `Authorization` (never sent to the snapshot download host), `User-Agent` and, for uploads, `Content-Length` |
| `body` | `bytes`, an iterable of `bytes` chunks (uploads, streamed) or `None` |
| `timeout` | The per-socket-operation timeout in seconds, already raised for held calls. `None` for uploads, `files.write` above 1 MiB and the realtime stream: bound their connect yourself |
| `stream` | `True` when the caller reads the body incrementally (realtime, snapshot downloads) and may call `close()` from another thread to abort it |

The returned object needs `status`, `read()`, `readline()` and `close()`: an `http.client.HTTPResponse` qualifies. Retries, error mapping and the `{status, response}` unwrapping stay in the client, so a transport only moves bytes.

`HTTPTransport(timeout=30.0)` is the default transport: one keep-alive connection per thread and host, gzip-compressed responses (except for streams), and `timeout` as the connect bound of the calls without their own timeout. You can wrap it:

```python theme={"system"}
import os
import time

from squarecloud import HTTPTransport, SquareCloud

default = HTTPTransport()


def timed(method, url, headers, body, timeout, stream):
    started = time.perf_counter()
    response = default(method, url, headers, body, timeout, stream)
    print(method, url, response.status, f"{time.perf_counter() - started:.3f}s")
    return response


client = SquareCloud(os.environ["SQUARECLOUD_API_KEY"], transport=timed)
```

`client.close()` closes the connections of the default transport. A custom transport that has a `close()` method is closed too.

### Logging

The SDK logs each request at `DEBUG` on the `squarecloud` logger: method, path and status, **never** bodies or keys. It only attaches a `NullHandler`, so nothing is printed until you configure logging:

```python theme={"system"}
import logging

logging.basicConfig()
logging.getLogger("squarecloud").setLevel(logging.DEBUG)
```

## Next steps

<CardGroup cols={2}>
  <Card title="Managing applications" icon="server" href="/en/sdks/py/managing_application">
    Status, lifecycle, logs and metrics.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/en/sdks/py/errors">
    Error class, retries and rate limits.
  </Card>

  <Card title="API introduction" icon="code" href="/en/api-reference/introduction">
    Base URL, authentication and a first request.
  </Card>

  <Card title="CLI quickstart" icon="terminal" href="/en/cli-reference/quickstart">
    Deploy and manage apps from the terminal.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.