mitra-sdk (Python)
Python client for the Matih public API. Sync (Mitra) and async (AsyncMitra) clients on
httpx, pydantic v2 models for the main response shapes, SSE
streaming, background-job polling and webhook signature verification.
Pre-release (0.1.0): the package is not on PyPI until
/api/v1is live for every workspace. Install from a checkout withpip install -e sdks/python.
pip install mitra-sdk # once released
Python 3.10+. The package imports as mitra.
Full documentation: matih.ai/docs/api, with the SDK reference.
Authenticate
from mitra import Mitra
client = Mitra(api_key="mtk_...") # or set MATIH_API_KEY
client = Mitra(access_token="mto_...", workspace_id="ws_123") # OAuth token
API keys are pinned to one workspace. OAuth tokens pick a workspace per client with
workspace_id= (sent as X-Matih-Workspace) or per call, or set a default with
client.workspaces.use(workspace_id). Keep keys in environment variables, never in source.
Admins create keys under Settings › API & MCP, choose the products the key may use (all on by default) and revoke keys in the same screen. Keys never expire by default. Use an API key here; an MCP connector key works only on /api/v1/mcp. Scopes: workspaces:read, members:read, members:manage, sources:read, sources:write, files:read, files:write, gems:read, gems:generate, ask, dashboards:read, predict, embeds:create, embeds:read, webhooks:manage.
Ask
answer = client.ask("What were last month's sales by region?", language="en-IN")
if answer.status == "answered":
print(answer.answer)
for row in answer.result.rows:
print(row)
if answer.image_url:
open("chart.png", "wb").write(client.download_image(answer.image_url))
elif answer.status == "clarification":
print(answer.clarification.question, answer.clarification.options)
# reply in the same conversation
answer = client.ask(answer.clarification.options[0], conversation_id=answer.conversation_id)
follow_up = client.ask(answer.follow_ups[0], conversation_id=answer.conversation_id)
Questions may be in any of 22 Indian languages and are auto-detected; pass language="hi-IN" to
force the response language. Data values, column names and figures are never translated.
Voice notes: answer = client.ask_audio("note.ogg", language="hi-IN"); answer.heard holds the
transcript and detected language.
Streaming
for event in client.ask_stream("Top 5 products by revenue"):
if event.event == "progress":
print("...", event.data)
elif event.event == "answer":
answer = event.as_answer()
elif event.event == "error":
raise RuntimeError(event.data)
Gems, dashboards, forecasts
gems = client.gems.list(source_id="src_1")
png = client.gems.image(gems.gems[0].id) # bytes
job = client.gems.generate("src_1") # waits, returns the finished Job
job = client.gems.generate("src_1", background=True) # returns the queued Job at once
job = client.jobs.wait(job, 600) # poll it later
dashboards = client.dashboards.list()
png = client.dashboards.render(dashboards.dashboards[0].id) # job + wait + download
job = client.predictions.create("Forecast next quarter's revenue", source_id="src_1")
print(job.result["answer"], job.result.get("attachments"))
jobs.wait(job, timeout=1860, poll_interval=1.0, max_poll_interval=15.0, on_update=fn) raises
JobTimeoutError (the job keeps running) or JobFailedError (failed or cancelled). Predictions
can take up to 30 minutes. ask and predictions.create send an Idempotency-Key automatically
and reuse it across retries, so a retry is never charged twice (the server honours the key on
these two calls only). Pass idempotency_key= to control it.
Sources and files
client.sources.list()
client.sources.get("src_1") # includes readiness
client.sources.connectors()
client.sources.test({"connector_type": "postgres", "display_name": "Orders DB", "host": "...", "database": "..."})
try:
client.sources.create({...}, activate=True) # admin, sources:write
except SourceTestFailedError as e: # HTTP 422
print(e.test_result)
client.sources.activate("src_1"); client.sources.delete("src_1")
# files: files:read to list and get; files:write to upload, answer a duplicate prompt and delete
f = client.files.upload("sales.csv", on_duplicate="ask") # CSV or Excel; "ask" (default) or "keep_both"
client.files.get(f.id) # status, decision_required, decision_options
client.files.duplicate(f.id, "use_existing") # answer a duplicate prompt: use_existing, keep_both or cancel
client.files.list(); client.files.delete(f.id)
Members and invitations
Needs members:read / members:manage; only do these when a person asked for them.
client.members.list()
client.invitations.create("user", email="new@example.com") # or phone="+91..."
client.invitations.list(); client.invitations.revoke("inv_1")
client.members.update("mem_1", "admin"); client.members.remove("mem_1")
Embeds
Show a dashboard (with per-viewer row filters) or a Gems feed inside your own product. Needs
embeds:create (mint, revoke) or embeds:read (list). Call it from your backend: the API key
never goes to the browser. The browser gets only the short-lived token, through the
<matih-embed> element (@matihlabs/embed, see sdks/embed/).
embed = client.embeds.create(
resource={"type": "dashboard", "id": "d_123"}, # or {"type": "gems", "id": "<source id>"}
filters={"customer_id": "C123"}, # exactly the dashboard's embed parameters; none for Gems
origins=["https://app.customer.com"], # 1 to 10 exact origins that may frame the page
ttl_seconds=300, # 60 to 900, default 300; never renewed, mint again
)
embed.src # the iframe URL without the token (keeps ?e=emt_...)
embed.token # met_..., a secret; left out of repr
embed.id # emt_..., not secret: pass it to revoke
client.embeds.list() # tokens minted by this key (never the secret or filter values)
client.embeds.revoke(embed.id) # 204, idempotent, returns None
Minting is not idempotent (every call mints) and is never retried after a 5xx. A refusal raises
EmbedRefusedError (422) with .code one of missing_parameter, unknown_parameter,
invalid_value, invalid_origin, filters_not_supported (Gems cannot be filtered),
no_embed_parameters; an unknown or unreadable dashboard or source raises EmbedNotFoundError
(404); a role that no longer allows it is a PermissionDeniedError (403). AsyncMitra is the
same with await.
Webhooks
hook = client.webhooks.create("https://example.com/hooks/matih", ["prediction.completed"])
print(hook.secret) # whsec_..., shown once
client.webhooks.test(hook.id); client.webhooks.deliveries(hook.id)
# Deliveries carry Matih-Signature, Matih-Event and Matih-Delivery headers.
# In your web handler, with the RAW request body:
from mitra import verify_webhook
if not verify_webhook(secret, request.headers["Matih-Signature"], raw_body): # constant-time
return 400
# assert_webhook(...) is the same check but raises WebhookVerificationError with the reason.
Sync vs async
Two independent choices: the client (Mitra blocks the thread, AsyncMitra is awaited) and the
mode of each long call (wait for the result, or get a job back and come back later). Both
clients have the same methods, including members, sources, files, webhooks, voice and jobs.
Every long call (gems.generate, predictions.create, dashboards.image, sources.create,
files.upload) takes:
wait=seconds (default 10 minutes for Gems, 5 for images, sources and files, 31 for predictions). The SDK sendsPrefer: wait=N(the server honours at most 60 s for jobs, 120 s for sources and files) and keeps polling until the result is final orwaitruns out.- Job calls (
gems.generate,predictions.create,dashboards.image) poll the job. Running out raisesJobTimeoutError; the job keeps running. - Resource calls (
sources.create,files.upload) poll the source or the file. Running out raises nothing: you get the source or file as it stands (for examplephase: building), so checkreadiness.can_askorstatus.
- Job calls (
background=Trueto return right away: the queuedJob, or forsources.createandfiles.uploadwhatever the server answered. NoPrefer: waitis sent.
job = client.predictions.create("Forecast Q4", wait=900) # blocks, finished Job
job = client.predictions.create("Forecast Q4", background=True) # returns immediately
job = client.jobs.wait(job.id, 900) # later, or from another process
queued = client.ask("Top customers this year", background=True) # Prefer: respond-async
done = client.jobs.wait(queued.id, 120)
answer = done.as_answer() # the Answer held in job.result
Without background=True, ask answers in one call. predictions.run and
dashboards.render are shortcuts for the waiting form (render also downloads the PNG).
Async
import asyncio
from mitra import AsyncMitra
async def main():
async with AsyncMitra(api_key="mtk_...") as client:
answer = await client.ask("How many orders today?")
job = await client.predictions.create("Forecast Q4", background=True)
job = await client.jobs.wait(job.id, 900)
async for event in client.ask_stream("Top customers"):
print(event.event)
asyncio.run(main())
Errors
All API errors derive from MitraError with .status, .category, .code, .request_id and
.body. Subclasses: AuthError (401), PermissionDeniedError (403: scope, workspace_role,
source_access), CreditsError (402: not_ready, member_cap), BusyError (409, a conflict with the current state),
RateLimitError (429, .retry_after), EmbedRefusedError (422 from embeds.create, read .code), EmbedNotFoundError (404 from embeds.create and embeds.revoke), SourceTestFailedError (422 from sources.create, .test_result), plus JobFailedError, JobTimeoutError. 429 is retried
automatically (honouring Retry-After) up to max_retries (default 2), as are 5xx and network
errors on GETs and on calls with an idempotency key.
Models
mitra.models holds pydantic models for the main shapes (Answer, Job, Gem, Workspace, ...).
They accept unknown fields, so new server fields never break old clients. See
scripts/README.md for generating models from the OpenAPI spec.
Develop
pip install -e "sdks/python[dev]"
pytest && ruff check .
License: Apache-2.0.
Metadata
Release files for mitra-sdk 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 | Provenance | Actions |
|---|---|---|---|---|
| mitra_sdk-0.1.0.tar.gz | 40.2 kB | Without provenance | Details for mitra_sdk-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| mitra_sdk-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for mitra_sdk-0.1.0-py3-none-any.whl |
Total release size: 72.1 kB
Release files / mitra_sdk-0.1.0.tar.gz
| Download URL | mitra_sdk-0.1.0.tar.gz |
|---|---|
| Size | 40.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9ecd832310471b5685e12d3f9716991b4b38b6440b63da44f237544dbfc875ec
|
|
BLAKE2b-256 checksum How to use checksums |
b81cf7e145cdddee69456764b5d8fe50ae804e625fe48be798bed3f474ffbca6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / mitra_sdk-0.1.0-py3-none-any.whl
| Download URL | mitra_sdk-0.1.0-py3-none-any.whl |
|---|---|
| Size | 31.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
705097939d1229984882c1d5b13335896da7bbd4634791d374b101f3606abf2c
|
|
BLAKE2b-256 checksum How to use checksums |
4a38d30c348fa65ffedff7809a125f23d3743c90fec339278c943ba7e3c8d840
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for mitra-sdk 0.1.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.