unofficial-mural-python-client
Unofficial, community-maintained project. Not affiliated with, endorsed by, or supported by MURAL. For official resources, see developers.mural.co.
A typed Python client for the MURAL Public API, covering all 76 endpoints of the OpenAPI 3.1 spec — murals, widgets (all 10 types), rooms, workspaces, templates, search, and user identity.
- Spec-accurate: every request/response model is a pydantic v2 model hand-derived from
the spec — camelCase wire format, snake_case Python attributes, enums as
Literals. - Client-side validation: request bodies are validated before the HTTP call, so malformed input fails fast instead of via a 400.
- OAuth2 built in: authorization-code flow, automatic token refresh on 401, and an interactive localhost helper for getting your first token.
- Transparent pagination: every list endpoint has a raw single-page method and an
iter_*generator that followsnextcursors. - Typed errors: HTTP errors map to an exception hierarchy carrying the parsed MURAL error body.
Install
uv add unofficial-mural-python-client
Install the latest development version
To track main (or a specific tag) instead of the latest PyPI release:
uv add git+https://github.com/franpandol/unofficial-mural-python-client
Pin to a tag for reproducible installs, e.g.
git+https://github.com/franpandol/unofficial-mural-python-client@v0.1.0.
Contributing / running from source
git clone https://github.com/franpandol/unofficial-mural-python-client
cd unofficial-mural-python-client
uv sync --extra dev
Set up a MURAL OAuth app
- Sign in at developers.mural.co and create a new app.
- Add the redirect URI
http://localhost:8765/callback(used by the local OAuth helper). - Select the scopes you need — least privilege is supported; e.g. a read-only integration
only needs
murals:read+identity:read. - Copy
.env.exampleto.envand fill inMURAL_CLIENT_ID,MURAL_CLIENT_SECRET, andMURAL_REDIRECT_URI.
If MURAL shows "The redirect URL is invalid", the app dashboard does not allow the exact
MURAL_REDIRECT_URI being sent. For the quickstart, add http://localhost:8765/callback
exactly: http (not https), localhost (not 127.0.0.1), port 8765, path
/callback, and no trailing slash.
If MURAL shows "Required permission scopes not recognized", add every requested scope to
that same app's permission-scope allowlist. The quickstart requests identity:read,
murals:read, and murals:write; murals:write is required because the smoke test
creates widgets and deletes the test mural.
Quickstart
from mural import MuralClient, MuralOAuth, run_local_oauth_flow
from mural.models import CreateMuralRequest, CreateStickyNoteWidget
oauth = MuralOAuth.from_env(scopes=["identity:read", "murals:read", "murals:write"])
token = run_local_oauth_flow(oauth) # opens the browser, catches the redirect
with MuralClient(token, oauth=oauth) as client:
me = client.users.me()
mural = client.murals.create(CreateMuralRequest(room_id=1234567890, title="Retro"))
client.widgets.create_sticky_notes(
mural.id,
[CreateStickyNoteWidget(shape="rectangle", x=100, y=100, text="Went well ✅")],
)
for widget in client.widgets.iter_list(mural.id):
print(widget.type, widget.id)
Or run the end-to-end smoke test (creates and deletes a real mural):
python examples/quickstart.py --room-id <numeric-room-id>
See examples/README.md for the full set of runnable examples and a
suggested reading order.
CLI
The package installs a Typer-powered mural command:
uv sync --extra dev
uv run mural --help
Authenticate once with the same OAuth environment variables used by the SDK:
uv run mural auth login \
--scope identity:read \
--scope murals:read \
--scope murals:write
uv run mural auth whoami
Common commands print JSON and reuse the SDK's typed request/response models:
uv run mural workspaces list --limit 10
uv run mural workspaces rooms <workspace-id>
uv run mural rooms create --workspace-id <workspace-id> --name "Project room" --type private
uv run mural murals create --room-id <numeric-room-id> --title "Retro"
uv run mural widgets create-sticky-note <mural-id> --text "Went well" --x 100 --y 100
uv run mural search murals <workspace-id> --query "retro"
By default, OAuth tokens are stored at ~/.config/mural/token.json with private file
permissions. Use --token-path <path> on auth and API commands to override it.
Token persistence and auto-refresh
MuralClient refreshes the access token once on a 401 when built with an oauth and a
token that has a refresh_token. Persist rotated tokens via the callback:
client = MuralClient(token, oauth=oauth, on_token_refresh=save_token_somewhere)
Pagination
page = client.workspaces.list(limit=25) # one page: page.value, page.next
for ws in client.workspaces.iter_list(): # all pages, transparently
print(ws.name)
Errors
from mural import MuralNotFoundError
try:
client.murals.get("workspace1234.000")
except MuralNotFoundError as error:
print(error.status_code, error.error.code, error.error.message)
MuralBadRequestError (400), MuralAuthError (401), MuralPermissionError (403),
MuralNotFoundError (404), MuralRateLimitError (429), MuralServerError (5xx) — all
subclasses of MuralAPIError.
Resource map
| Attribute | Endpoints |
|---|---|
client.murals |
CRUD, duplicate, export, access info, assets, private mode, chat, tags, timer, users/permissions, visitor settings, voting sessions |
client.widgets |
list/get/delete + typed create/update per type: sticky note*, shape*, textbox*, title*, area, arrow, comment, file, image, table |
client.rooms |
CRUD, folders, murals listing, members, invite/remove |
client.workspaces |
list/get, murals, rooms, templates, invite |
client.templates |
default templates, create-from-mural, delete, create-mural-from-template |
client.search |
murals, rooms, templates within a workspace |
client.users |
/users/me |
* bulk endpoints — take a list of bodies, return a list of created widgets, matching the API.
Tests
pytest --cov # mocked with respx; no network, 98 tests, 99% coverage
Release files for unofficial-mural-python-client 0.1.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 | |
|---|---|---|---|
| unofficial_mural_python_client-0.1.0.tar.gz | 84.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| unofficial_mural_python_client-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 125.8 kB
Release files / unofficial_mural_python_client-0.1.0.tar.gz
| Download URL | unofficial_mural_python_client-0.1.0.tar.gz |
|---|---|
| Size | 84.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5cc6040ac04dd7ed89ed8aae4a46e4ec4a04f60287311a362d31783959ed3fd4
|
|
BLAKE2b-256 checksum How to use checksums |
50c62f2dde170b25f8be32edf08d34f00edecbe1de00f7eeaed792cbb781ee78
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.27 {"installer":{"name":"uv","version":"0.11.27","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 / unofficial_mural_python_client-0.1.0-py3-none-any.whl
| Download URL | unofficial_mural_python_client-0.1.0-py3-none-any.whl |
|---|---|
| Size | 41.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c6b7f4713c244b691e93409662c59faef755e7d2c24f7c056492cd419f7055c5
|
|
BLAKE2b-256 checksum How to use checksums |
6242795baa60f7c1919bd630cd1cc44655c655bfc88ef113f916ac468eeb2254
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.27 {"installer":{"name":"uv","version":"0.11.27","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}
|