Skip to main content

multinodal

The Python client for the Multinodal API. Multinodal builds a knowledge graph from your Documents. Each Relationship in the graph carries Evidence: the verbatim quote that supports it. This client uploads Documents, waits for processing, reads the graph, and writes exports.

This guide takes you from installation to an export file on disk.

Contents

  1. Install
  2. Create an API key
  3. Configure the client
  4. Upload Documents
  5. Wait for processing
  6. Read Relationships and their Evidence
  7. Export the graph
  8. Use the async client
  9. Handle errors
  10. Retries and timeouts
  11. Known gaps
  12. Recipe notebook
  13. License

1. Install

The client needs Python 3.10 or later. Its only runtime dependencies are httpx and pydantic.

pip install multinodal

Release status. Version 0.1.0 is not on PyPI yet. The owner publishes it. Until then, install from a clone of this repository:

git clone https://github.com/multinodal-ai/python-sdk.git
pip install ./python-sdk

2. Create an API key

  1. Sign in at https://multinodal.dev.
  2. Open Settings → API Keys.
  3. Select Create new key, and enter a name.
  4. Select the access level.
    • Read only reads Collections, Documents, the graph, exports, and usage.
    • Read and write also creates, changes, and deletes Collections and Documents. It spends Page Credits.
  5. Copy the key. It starts with mn_. The page shows it once.

This guide uploads Documents, so it needs a Read and write key. A write with a Read only key fails with the forbidden error.

Processing spends Page Credits. A Document costs one Page Credit per page, at the rate of its Pricing Band. PDF and DOCX use Band A. TXT and Markdown use Band B. A failed Document costs nothing. Top up your Credit Balance in the webapp before you upload.

3. Configure the client

Set the key in the environment:

export MULTINODAL_API_KEY="mn_..."

Create the client:

from multinodal import Multinodal

mn = Multinodal()

The client reads two environment variables.

Variable Required Default
MULTINODAL_API_KEY Yes None
MULTINODAL_BASE_URL No https://api.multinodal.dev

You can pass both values as arguments instead:

mn = Multinodal(api_key="mn_...", base_url="https://api.multinodal.dev")

Do not hard-code the host. The API host can move to a different domain. The client follows MULTINODAL_BASE_URL, so a move needs no code change.

Check your Credit Balance before you spend it:

usage = mn.usage.me()
print(usage.balance_micros)  # an int, in USD micros
print(usage.balance)  # the same amount as a Decimal, in USD

Money is always an int of USD micros, or a Decimal. It is never a float.

4. Upload Documents

Create a Collection. A Collection holds Documents and one knowledge graph. Entities are reconciled across all Documents in the same Collection.

col = mn.collections.create(name="Filings")
print(col.id)  # save this id to reopen the Collection later

A Collection name has 1 to 30 characters.

Upload Documents from local paths:

batch = col.documents.upload(["a.pdf", "b.pdf"])

The accepted formats are PDF, TXT, Markdown, and DOCX. One call accepts up to 10 Documents. upload returns when the server accepts the Documents. Processing then runs in the background.

To reopen a Collection later, pass its saved id:

col = mn.collection(saved_id)  # a handle; it makes no network call

5. Wait for processing

documents = batch.wait_until_done(timeout=600)
for doc in documents:
    print(doc.filename, doc.status)

wait_until_done returns when every Document in the batch reaches a terminal status.

Status Meaning Billed
DONE Processing succeeded. Yes
DONE_WITH_WARNINGS Processing succeeded with degraded extraction. Yes
ERROR Processing failed. doc.error_message holds the reason. No

A Document that only partly succeeds is billed in full.

The client does not poll. It follows the event stream (GET /api/v1/events) over server-sent events. It opens the stream first, then reads each Document once, so it misses no event. The server keeps events for 7 days, and the stream replays missed events in order. A dropped connection resumes from the last event id.

The call raises WaitTimeout, a subclass of TimeoutError, when timeout seconds pass first. Processing continues on the server. Call wait_until_done again to keep waiting.

6. Read Relationships and their Evidence

A Relationship is a directed, typed assertion between two Entities. Each one carries Evidence: the verbatim quote from the Document text that supports it.

for rel in col.graph.relationships(type="works_at"):
    print(rel.subject.name, rel.type, rel.object.name)
    print("  Evidence:", rel.evidence.text)
    print("  offset:", rel.evidence.offset, "verified:", rel.evidence.verified)
  • type filters on one relation type. Omit it to read every Relationship.
  • Relation types are plain strings. The Documents produce them, so no fixed list exists.
  • The iterator fetches pages as you read. It does not load the whole graph at once.
  • rel.evidence.verified is True when the pipeline found the quote in the Document text. A quote that matches only after pronouns are resolved also counts as verified.
  • rel.evidence.offset is the character position of the quote in the Document's extracted text. It is None when the quote matched only after pronoun resolution, or did not match.

Inferred Relationships are a separate type. They come from reasoning over other Relationships, not from the text. They carry no Evidence.

for inf in col.graph.inferred_relationships():
    print(inf.subject.name, inf.type, inf.object.name, inf.rule, inf.hops)

Check for an empty graph before you read it:

if col.graph.is_empty():
    print("Processing finished, but no Relationships were extracted.")

A Collection can finish processing with no Relationships. See Known gaps.

Every model is plain Pydantic data underneath:

col.model_dump()

7. Export the graph

col.export("cypher", to="graph.cypher")

The export streams to the file in chunks. If the connection breaks mid-stream, the client deletes the partial file and raises IncompleteExport. You never get a truncated file that looks complete.

Format Argument Write it to
Cypher "cypher" graph.cypher
JSON-LD "jsonld" graph.jsonld
Turtle "turtle" graph.ttl
GraphML "graphml" graph.graphml
NetworkX Parquet "parquet" graph.zip

The parquet export is a ZIP file that holds nodes.parquet and edges.parquet. The server builds it in memory before it sends the first byte. A large Collection waits longer before the download starts.

You now have an export file on disk.

8. Use the async client

AsyncMultinodal has the same surface as Multinodal. Await each call, and use async for on each iterator.

import asyncio
from multinodal import AsyncMultinodal


async def main() -> None:
    amn = AsyncMultinodal()
    col = await amn.collections.create(name="Filings")
    batch = await col.documents.upload(["a.pdf", "b.pdf"])
    await batch.wait_until_done(timeout=600)
    async for rel in col.graph.relationships(type="works_at"):
        print(rel.subject.name, rel.evidence.text)
    await col.export("cypher", to="graph.cypher")


asyncio.run(main())

A handle from amn.collection(saved_id) holds only the id. Call await col.load() before you read its fields, such as col.name. A handle that create, get or update returns is already loaded.

Use AsyncMultinodal inside async code. The sync wait_until_done can block for minutes. Inside an event loop, that blocks every other task.

9. Handle errors

Every API error raises a subclass of MultinodalError. The error carries the RFC 7807 problem fields: type, title, status, detail, and instance. A validation-failed error also carries errors, one entry per invalid field.

Match on the slug, not on the full type URI. The slug is the last segment of type, such as out-of-credits. The slug never changes. The host in the URI can change.

from multinodal import MultinodalError, OutOfCredits

try:
    batch = col.documents.upload(["a.pdf"])
except OutOfCredits:
    print("Top up your Credit Balance at https://multinodal.dev.")
except MultinodalError as err:
    print(err.slug, err.status, err.detail)

The client maps each slug to its own exception class. An unknown slug raises MultinodalError itself.

Slug Status What to do
unauthorized 401 Set MULTINODAL_API_KEY.
invalid-key 401 Check the key. Create a new one if you lost it.
key-revoked 401 Create a new key.
forbidden 403 Use a Read and write key for writes.
not-found 404 Check the id. A key reads only its own owner's resources.
validation-failed 400 Read err.errors for the invalid fields.
invalid-cursor 400 Restart the iteration.
out-of-credits 402 Top up your Credit Balance.
budget-exceeded 402 Raise your Budget, or wait for the next period.
credits-exhausted 402 Top up your Credit Balance.
document-too-large 413 Split the Document.
unsupported-file-type 415 Convert it to PDF, TXT, Markdown, or DOCX.
document-not-retryable 409 Only a failed Document can be retried.
rate-limit-exceeded 429 The client retries it for you. See below.
events-expired 410 The client handles it inside wait_until_done.
pricing-uncalibrated 503 Try again later.
internal-error 500 Try again later. Report it if it persists.

Each type URI opens a page that describes the error. The API host lists every slug at /errors/.

10. Retries and timeouts

The API has no idempotency keys. So the client retries only a request that is safe to send twice.

Method Retried on
GET, DELETE 429, 502, 503, 504, and connection errors
POST, PATCH 429 and connection errors only

A connection error means the request never reached the server. A 429 means the server refused it before doing any work. Both are safe to resend.

A timed-out upload request is not resent. The server may have received it and started processing. A second send could process and bill the Document twice. After an upload request times out, list the Collection's Documents before you upload again:

for doc in col.documents.list():
    print(doc.filename, doc.status)

Backoff is exponential with full jitter. The client honours the Retry-After header on a 429.

The client sets separate connect, read, write, and pool timeouts. You can override them per call.

11. Known gaps

These are current limits of version 0.1.0. None of them is a promise of a fix.

  • Evidence does not name its source Document. Evidence carries text, offset, and verified. It carries no Document id. In a Collection with several Documents, you cannot trace a quote to the Document it came from. offset locates the quote only inside that unnamed Document.
  • Inverse relation types are not collapsed. works_at and employs stay two separate types. A filter on works_at does not return assertions recorded as employs. To read a relationship completely, query each direction by name. The measured rate of inverse pairs is low, and collapsing them would break existing queries.
  • A Document can finish with an empty graph. A DONE Document can yield no Relationships. Check col.graph.is_empty(). An empty graph is not a client error.
  • Parquet export is buffered on the server. The other four formats stream. Parquet waits until the whole file is built.
  • The relation-type vocabulary is unbounded. Types come from the Documents. Discover them from the graph you receive, not from a schema.

12. Recipe notebook

notebooks/retrieval_over_the_graph.ipynb shows retrieval over the graph. It uses a frozen corpus of two public-domain PDFs in notebooks/corpus/. Running it spends 8 Page Credits at the Band A rate. The notebook is a recipe for you to run and measure yourself. It makes no claim about accuracy or quality.

13. License

Apache License 2.0. See LICENSE.

Release files for multinodal 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for multinodal 0.1.0
File Size Uploaded
multinodal-0.1.0.tar.gz 59.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for multinodal 0.1.0
File Interpreter ABI Platform
multinodal-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 100.1 kB

Release files / multinodal-0.1.0.tar.gz

Download URL multinodal-0.1.0.tar.gz
Size 59.4 kB
Tags Source
SHA-256 checksum
How to use checksums
0fd36ff4c78088c82432f8fbd6535ef0498e6d4280e3ceb4a071d39d65880867
BLAKE2b-256 checksum
How to use checksums
a84096c5d36e7262359b9f086834639873808ddec75d3f3aa1e31db2b736bdc2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release files / multinodal-0.1.0-py3-none-any.whl

Download URL multinodal-0.1.0-py3-none-any.whl
Size 40.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0297fb81d963cec2258ca1fecd943552d82603505b9906abe784c324265fa3cb
BLAKE2b-256 checksum
How to use checksums
281dc78feaf92bc7c5d8f8eb56c2dad63109cf0a42cc5518a5e95998f35c7305
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page