Ouro
  • Docs
  • Blog
  • Pricing
  • Teams
Sign inJoin for free

Get started

Overview
Introduction
Onboarding

Platform

Introduction
Economics
Teams
Organizations

Developers

Introduction
Quickstart
Libraries
MCP interface
API reference
Python

Concepts

Files
Datasets
Services
Routes
Posts
Quests
Conversations
Extended markdown
USD Payments
Bitcoin

Python SDK for the Ouro API

Learn how to interact with Ouro from Python.


MCP interfaceFiles

© 2026 Ouro Foundation

On this page

  • Assets
  • Files
    • List files
    • Create a file
    • Read a file
    • Update a file
    • Delete a file
  • Datasets
    • List datasets
    • Create a dataset
    • Read a dataset
    • Read schema
    • Query data by dataset ID
    • Run SQL against a dataset
    • Update a dataset
    • Delete a dataset
  • Services
    • Create a service
    • Read a service
    • Update a service
    • Delete a service
  • Routes
    • Create a route
    • Read a route
    • Update a route
    • Delete a route
    • Execute a route
    • Read a route's actions
  • Posts
    • List posts
    • Create a post
    • Read a post
    • Update a post
    • Delete a post
  • Quests
    • Create a quest
    • Retrieve and update a quest
    • Quest items and entries
    • Entry submission limits (type)
    • Delete a quest
  • Conversations
    • Create a conversation
    • List conversations
    • Retrieve a conversation
    • Update a conversation
    • Create a message
    • List messages
    • Delete or leave a conversation
  • Comments
    • Create a comment
    • List comments for an asset
    • Retrieve a comment
    • Update a comment
    • Create and list replies

Before working with any of the methods outlined below, you'll need to initialize the Ouro client. Make sure you've updated ouro-py to the latest version as we are consistently making updates. More details can be found in the quickstart.

python
import os
from ouro import Ouro
 
ouro = Ouro(api_key=os.environ.get("OURO_API_KEY"))

Assets

Ouro assets include files, datasets, services, routes, posts, quests, and conversations. Pass org_id and team_id when creating assets so they land in the right team (see Teams).

Skip to one of the sections below:

Files

Upload, read, update, and delete files

Datasets

Structured tabular data with SQL storage

Services

External APIs and route endpoints

Posts

Rich text content with the Editor class

Quests

Team quests, items, entries, and reviews

Conversations

Create threads and exchange messages

For every asset on the platform, Ouro stores the following information:

  • ID
  • Name
  • Description (optional)
  • Metadata (asset specific details, like file size or number of rows)
  • Visibility (public, private, monetized, or organization)
  • Created at
  • Updated at
  • Owner
  • Organization
  • Team
  • Price (if monetized)

Unless you pass an organization and team, assets are created in your global organization and default team. Control who can see an asset with visibility.

Choosing the right team keeps activity grouped and helps the right people find the work.

Files

Files are the most basic asset on Ouro. Any file is fair game, and many file types have rich visualizations on the web platform.

List files

Browse or search your files with optional filters for scope, organization, and team.

python
# List your recent files
files = ouro.files.list()
 
# Search with a query
files = ouro.files.list(query="climate data", limit=10)
 
# Scope to an organization
files = ouro.files.list(org_id=org_id, team_id=team_id)

Create a file

You can upload any file, up to 5GB in size.

python
file = ouro.files.create(
    name="cif file",
    description="Test file",
    visibility="public",
    file_path="./Fe.cif",
)
print(file)

file_path is the path to the file on your local machine.

If you need to upload multiple files at once, currently the best way is to compress them into a .zip file and upload that.

If you run into issues uploading large files, you can upload them using the web interface. After upload, you'll be able to find the file ID in the details dropdown (cog icon) on the file page header. Once you have the file ID, you can use it to interact with the file programmatically.

Read a file

python
id = '48ec6563-5520-4756-ac7f-b38f4933ac95'
file = ouro.files.retrieve(id)
 
# file is an object with properties like:
# file.id, file.name, file.description, file.metadata, file.visibility, file.created_at, file.user

If you created the file from the SDK, the ouro.files.create response will be a file object which has the ID in the id field. If you uploaded the file using the web interface, or the asset was created by another user, you can find its ID in the file page header details dropdown (cog icon).

See the highlighted section in the screenshot below:

File ID in the details dropdown

Once you've retrieved a file object with ouro.files.retrieve, you can read its data using the read_data method.

python
file_data = file.read_data()
print(file_data.url)

The read_data method returns a FileData object which has a url property that you can use to download the file.

python
import requests
 
url = file_data.url
response = requests.get(url)
print(response.content)

Update a file

To be able to update a file, you must have admin or write permission on the file. As the creator of a file, you are automatically granted admin permissions.

python
file_id = file.id
updated_file = ouro.files.update(
    id=file_id,
    file_path="./Fe2BiNi.cif",
    name="Fe2BiNi (Pmmm) updated"
)
# Returns the updated file object
print(updated_file)

You can update the file object with any of asset properties using named parameters. Only id is required. file_path is a special property for files that allows you to update the file data with a new file. This is also optional.

Delete a file

You must have admin permissions on the file to delete it. This will completely remove the file from the platform.

python
ouro.files.delete(id=file.id)

Assets that may have referenced the file will no longer show a connection to the file.

Datasets

Datasets are structured tabular assets stored in the datasets schema. You can create them from a pandas DataFrame, then retrieve metadata, read the schema, and load/query the data.

List datasets

Browse or search datasets with optional filters.

python
datasets = ouro.datasets.list()
datasets = ouro.datasets.list(query="temperature", scope="org", limit=10)

Create a dataset

Provide a pandas DataFrame and required asset fields. The SDK will infer a SQL schema and upload a preview; if you pass data, rows are inserted into the table.

python
import pandas as pd
 
data = pd.DataFrame(
    [
        {"name": "Bob", "age": 30},
        {"name": "Alice", "age": 27},
        {"name": "Matt", "age": 26},
        {"name": "Bobo", "age": 4},
        {"name": "Asta", "age": 15},
    ]
)
 
dataset = ouro.datasets.create(
    name="preview-dataset",
    visibility="public",
    monetization="none",
    data=data,
)
print(dataset.id)

Notes:

  • name is converted to a SQL-safe table_name (spaces -> underscores, lowercase) stored in dataset.metadata["table_name"].
  • If you omit data, the dataset (asset + empty table) is created without rows.

Read a dataset

Retrieve the dataset object by ID to access standard asset fields plus dataset-specific metadata and preview rows.

python
dataset_id = "0194f68c-b16e-70d3-8ed3-aafa850272ae"
dataset = ouro.datasets.retrieve(dataset_id)
print(dataset.name, dataset.metadata, dataset.preview[:3])

Read schema

Get column definitions for the underlying table.

python
columns = ouro.datasets.schema(dataset_id)
for col in columns:
    print(col["column_name"], col["data_type"])  # e.g., age integer, name text

Query data by dataset ID

Fetch the dataset's rows as a pandas DataFrame via the Ouro API. Timestamp and date columns are parsed to pandas types.

python
df = ouro.datasets.query(dataset_id)
print(df.head())

Run SQL against a dataset

Pass a SQL string as the second argument to query() to run a read-only PostgreSQL query against the dataset's table. Reference the table as {{table}} - the backend rewrites the placeholder to the fully-qualified datasets."..." name. Read-only is enforced server-side and queries time out after 10 seconds. Include LIMIT/OFFSET directly in the SQL.

python
# Aggregations
totals = ouro.datasets.query(
    dataset_id,
    "SELECT species, count(*) AS n FROM {{table}} GROUP BY species ORDER BY n DESC",
)
 
# Time-series buckets
daily = ouro.datasets.query(
    dataset_id,
    "SELECT date_trunc('day', created_at) AS day, count(*) AS n "
    "FROM {{table}} GROUP BY 1 ORDER BY 1",
)
 
# Sample rows
sample = ouro.datasets.query(dataset_id, "SELECT * FROM {{table}} LIMIT 5")

Update a dataset

Update asset properties and optionally write rows to the table by passing a DataFrame. Writing new data will replace the existing data in the table.

python
updated = ouro.datasets.update(
    dataset.id,
    visibility="private",
)
 
# Write rows (optional)
data_update = pd.DataFrame([
    {"name": "Charlie", "age": 33},
])
updated = ouro.datasets.update(dataset.id, data=data_update)

Delete a dataset

Requires admin permission on the asset.

python
ouro.datasets.delete(dataset.id)

Tips:

  • Visibility controls who can access the dataset: public, private, monetized, or organization.
  • For IDs created in the web UI, find the ID in the dataset page header details dropdown.

Services

External APIs on Ouro are organized with two asset types: services and routes.

A service is a collection of routes that all share the same base URL. Routes are individual endpoints of the API defined by an HTTP method, path, optional URL and query parameters, and an optional body.

Ouro makes no guarantees about the stability of the APIs added to the platform. Make sure you trust the source/creator of the API before sending any sensitive data. You don't need to worry about the security of your account as no Ouro credentials are shared with the API.

If the API endpoint is monetized, you'll only be charged for successful requests.

Create a service

Not yet supported from the SDK. Use the web interface to create a service.

You can learn more about creating services with our guide How to monetize APIs. While focused on monetizing existing APIs, it covers the basics of creating and adding an API to Ouro.

Read a service

python
service_id = "438e454b-cf9e-40d9-b53d-b9b250087179"
service = ouro.services.retrieve(service_id)

Like the file object, using ouro.services.retrieve will return a service object with the same base asset properties.

Once you've retrieved a service object, you can interact with routes property.

python
# Returns a list of Route objects
service_routes = service.read_routes()
 
# Returns a dictionary with the stored OpenAPI specification as JSON
service_spec = service.read_spec()
print(service_spec['info']['title'])

The read_spec() method retrieves the stored OpenAPI specification from our database. This is the parsed specification that was either uploaded or fetched from a remote URL when the service was created. The spec is stored as a JSON object and remains stable even if the original remote spec changes, until the service is updated.

You can execute a service's endpoint with the execute_route method.

python
route = service_routes[0]
action = service.execute_route(
    route.id,
    body={"composition": "Fe2Ni", "temperature": 0.8, "max_new_tokens": 3000}
)
response = action.final_data

More details on using route functionality is in the routes section.

Update a service

Not yet supported from the SDK. Use the web interface to update a service. Navigate the asset and click the edit icon in the asset's header. You'll be able to adjust things like base URL, name, description, and authentication.

To update an individual route, navigate to the route and click the edit button to use the edit form.

Delete a service

Not yet supported from the SDK. Use the web interface to delete a service. Navigate the asset and click the delete button in the settings dropdown in the asset's header.

Routes

Routes are the individual endpoints of a service. Each route represents one HTTP endpoint of the underlying web API.

Create a route

Add a route to an existing service with ouro.routes.create. Pass the parent service_id and the fields your API expects (method, path, schemas, and so on). For complex OpenAPI-backed services, creating routes in the web UI is often easier.

python
route = ouro.routes.create(
    service_id,
    name="transcribe",
    method="POST",
    path="/transcribe",
)

Read a route

To get the details of a route, use the ouro.routes.retrieve method. You can supply either the route ID or the asset identifier, which consists of the creator and the route name plus method.

The identifier is the section of the URL after routes/. Unless you own the route, use the ID because it remains fixed even if the asset name changes. When an asset name changes, its identifier and URL also change.

python
route_id = "2443b425-a2bf-4a6f-8202-3ba8b36c921f" # CrystaLLM generate route
route = ouro.routes.retrieve(route_id)
 
# OR
 
route_identifier = "mmoderwell/post-generate"
route = ouro.routes.retrieve(route_identifier)
 
# Returns a Route object

For file-input routes, inspect the nested route metadata to see compatibility rules:

python
route = ouro.routes.retrieve(route_id)
 
print(route.route.input_type)             # e.g. "file"
print(route.route.input_filter)           # e.g. "audio"
print(route.route.input_file_extensions)  # e.g. ["xy", "xye"]

input_file_extensions is the preferred field for exact file-format matching. Older routes may still use the legacy single-value input_file_extension. For routes that declare multiple named inputs or outputs, see the route input and output assets guide.

Update a route

Update route metadata with ouro.routes.update. You can pass the route ID or asset identifier (same as retrieve).

python
route = ouro.routes.update(
    route.id,
    description="Transcribe audio files to text",
)

Delete a route

Not yet supported from the SDK. Use the web interface to delete a route.

Execute a route

You can execute a route with the execute method. It returns an Action object with the response, status, input/output assets, logs, and action ID. See Routes and actions for async runs, webhooks, and chaining.

python
route = ouro.routes.retrieve("mmoderwell/post-generate")
 
action = route.execute(
    body={
       "composition": "Fe2Ni", "temperature": 0.8, "max_new_tokens": 3000
    }
)
generation = action.final_data

Other supported parameters to execute include:

  • query: query string parameters
  • params: URL path parameters
  • input_assets: a dictionary mapping route input names to Ouro asset IDs
  • input_asset: a legacy single asset object to pass to routes with one asset input

You can chain multiple routes into custom workflows. Many routes create files, datasets, or posts as outputs. The Python SDK automatically adds saved output assets to action.final_data.

python
generation_route = ouro.routes.retrieve("mmoderwell/post-generate")
generation_action = generation_route.execute(
    body={
       "composition": "Fe2Ni", "temperature": 0.8, "max_new_tokens": 3000
    }
)
generation = generation_action.final_data
 
# Read the generated file from the action's final data.
file_id = generation["file"]["id"]
 
prediction_route = ouro.routes.retrieve("mmoderwell/post-magnetism-curie-temperature")
prediction_action = prediction_route.execute(
    input_assets={"file": file_id}
)
prediction = prediction_action.final_data
print(prediction)

For routes with multiple named outputs, read outputs by their declared names from action.final_data, action.output_assets, or the full action record.

Read a route's actions

Every time a route is executed, Ouro stores the request and response in an action object. Actions track which inputs created which outputs, including any assets created by the route.

python
actions = route.read_actions()
# Returns a list of Action objects
print(actions)

Actions are especially useful for long-running routes. If a route returns 202 Accepted, use the action ID with ouro.routes.retrieve_action, ouro.routes.poll_action, or action.refresh() to retrieve the result when it is ready.

Posts

Posts on Ouro are a way to share rich text-focused content with the Ouro community. Just like with every other kind of asset, you can create posts with a text editor on the web interface or use the SDK to create them programmatically.

List posts

Browse or search posts with optional filters.

python
posts = ouro.posts.list()
posts = ouro.posts.list(query="machine learning", scope="global", limit=5)

The SDK exposes an Editor class that allows you to construct a document block by block. Blocks are things like paragraphs, headings, lists, or references to other assets.

python
import pandas as pd
 
content = ouro.posts.Editor()
 
content.new_header(level=1, text="Hello World")
content.new_paragraph(text="This is a paragraph")
content.new_code_block(language="python", code="print('Hello, World!')")
content.new_table(pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}))
content.new_inline_asset(id="438e454b-cf9e-40d9-b53d-b9b250087179", asset_type="service", view_mode="card")
content.new_inline_asset(id="8891046c-b52c-432f-b9b0-ca9515bb1c20", asset_type="file", view_mode="preview")

Valid methods of the Editor class are:

  • new_header: adds a header block of specified level (1-3)
  • new_paragraph: adds a paragraph of text
  • new_code_block: adds a code block
  • new_table: adds a table from a pandas DataFrame
  • new_inline_asset: adds an Ouro asset with an optional visualization of the data (view_mode="preview")

Using the editor only creates a local object with your content. You can view your content as JSON and Markdown text using the to_dict method.

Create a post

Before you can create a post, you need to have the content you want to post. The Editor class explained above is one way. You can also create an Editor directly from a markdown string:

python
markdown_string = """
# Hello World
This is a test post from the Python SDK
"""
 
content = ouro.posts.Editor(text=markdown_string)

When the Editor is created via ouro.posts.Editor(), it's automatically connected to the Ouro client. Passing text to the constructor triggers server-side markdown parsing, which handles standard markdown as well as Ouro-specific syntax like @mentions and asset embeds.

You can also call from_markdown explicitly on an existing editor:

python
content = ouro.posts.Editor()
content.new_header(level=1, text="Hello World")
content.from_markdown("More content parsed from markdown")

This is especially useful for converting LLM output. You can ask the model to return Ouro extended markdown with a prompt like this:

Write responses in extended markdown. Mention users as @username, prefer typed asset links like [results](dataset:<uuid>), and use assetComponent blocks when the reader needs a rich preview.

markdown
```assetComponent
{
  "id": "<uuid>",
  "assetType": "dataset",
  "viewMode": "preview",
  "displayConfig": {
    "visualizationId": "<dataset-view-uuid-or-null>",
    "actionId": "<route-action-uuid-or-null>"
  }
}
```

When you embed an asset, you don't need to provide the title, description, or link; Ouro renders those from the asset. Set assetType to post, file, dataset, route, or service. For files and datasets, prefer preview; otherwise use card. For datasets, set displayConfig.visualizationId to render a saved dataset view. For routes, set displayConfig.actionId to pin a specific run.

Once you have your content, you save it to Ouro with the ouro.posts.create method.

python
post = ouro.posts.create(
    content=content,
    name="Hello World",
    description="This is a post from the Python SDK",
    visibility="public",
)
# Returns a Post object
print(post)

If you don't need block-by-block control, you can skip the Editor entirely and pass markdown or a file path directly:

python
post = ouro.posts.create(
    name="Hello World",
    content_markdown="# Hello World\nThis is a test post from the Python SDK",
    visibility="public",
)
 
# Or from a markdown file
post = ouro.posts.create(
    name="Hello World",
    content_path="./my-post.md",
    visibility="public",
)

Provide exactly one of content, content_markdown, or content_path.

Note: If you start your content with an H1, the name of the post should match the H1.

Read a post

You can read a post with the ouro.posts.retrieve method. You can find the ID of a post in the response of the ouro.posts.create method or from the web interface.

python
post_id = "0190ea44-bfef-7f8b-9e5f-503fc20a4d91"
post = ouro.posts.retrieve(post_id)
 
# Returns a Post object
print(post)

You will find the post's content in the content property of the post object. You can get a markdown representation of the content with the text property.

python
post_markdown = post.content.text
print(post_markdown)

Update a post

You can update a post with the ouro.posts.update method.

python
updated_post = ouro.posts.update(
    post.id,
    description="This is a post from the Python SDK that is now private",
    visibility="private"
)
 
# Returns the updated post object
print(updated_post)

You can update any of the post's properties, including the content, using the same approach used to create it.

Delete a post

You can delete a post with the ouro.posts.delete method. You must be an admin of the post to delete it.

python
ouro.posts.delete(post.id)

Quests

Quests are team-scoped requests for help. Use ouro.quests to create quests, manage items, submit entries, and review contributions. See Quests on Ouro for the product model.

Create a quest

python
quest = ouro.quests.create(
    name="CeO2 reference patterns",
    description="Collect reference XRD patterns for CeO2 nanoparticles.",
    visibility="organization",
    org_id=org_id,
    team_id=team_id,
    type="closable",
    status="open",
    items=[
        "Upload a clean .xy or .xye pattern",
        {"description": "Write a short methods note", "expected_asset_type": "post"},
    ],
)

Retrieve and update a quest

python
quest = ouro.quests.retrieve(quest_id)
ouro.quests.update(quest_id, status="closed")

Quest items and entries

python
items = ouro.quests.list_items(quest_id)
ouro.quests.create_items(quest_id, ["Additional benchmark file"])
 
entry = ouro.quests.create_entry(
    quest_id,
    item_id=items[0].id,
    asset_id=dataset.id,
    asset_type="dataset",
)
entries = ouro.quests.list_entries(quest_id, status="submitted")
ouro.quests.review_entry(quest_id, entry.id, status="accepted")

Entry submission limits (type)

Set type when creating a quest ("closable" default, or "continuous"):

typecreate_entry behavior
closableAt most one active entry per (item_id, your user) while status is submitted or accepted. A second call raises an API error. After rejection, you may submit again.
continuousNo per-user cap — each create_entry inserts a new row for the same item.
python
# Ongoing benchmark — contributors can submit every week
ouro.quests.create(
    name="Weekly structure upload",
    type="continuous",
    items=["Upload this week's relaxed structure"],
    org_id=org_id,
    team_id=team_id,
)
 
# One-shot bounty — one pending/accepted slot per contributor per item
ouro.quests.create(
    name="Reference XRD pattern",
    type="closable",
    items=["Upload a clean .xy pattern"],
    org_id=org_id,
    team_id=team_id,
)

Inspect quest.quest.type (nested on the retrieved asset) before retrying failed submits. Use assets={key: asset_id} for multi-input items; see Quests on Ouro.

Delete a quest

python
ouro.quests.delete(quest_id)

Conversations

Conversations let users exchange messages. You can create, list, retrieve, update, and delete conversations, and create or list messages within a thread.

Create a conversation

Start a conversation by passing member user IDs. Include yourself if you want the thread to appear in your conversation list.

python
conversation = ouro.conversations.create(
    member_user_ids=[my_user_id, teammate_user_id],
    name="Project Alpha",
    org_id=org_id,
    team_id=team_id,
)

List conversations

Start by listing your conversations to find threads you want to work with.

python
conversations = ouro.conversations.list()
for c in conversations:
    print(c.id, c.name, c.metadata)

Retrieve a conversation

Once you have an ID, load the conversation to inspect metadata and access message helpers.

python
conversation_id = "0190ea44-bfef-7f8b-9e5f-503fc20a4d91"
conversation = ouro.conversations.retrieve(conversation_id)
print(conversation.name, conversation.metadata)

Update a conversation

You can update top-level fields like name and summary.

python
updated = ouro.conversations.update(
    conversation_id,
    name="Project Alpha",
    summary="Research thread"
)
print(updated)

Create a message

Send a message to a conversation as plain text or structured JSON. Use the conversation.messages.create helper.

python
# Text message
msg = conversation.messages.create(text="Hello team!")
 
# JSON message (rich content)
editor = ouro.posts.Editor()
editor.new_paragraph(text="Hello team!")
msg2 = conversation.messages.create(json=editor.to_dict())

List messages

Read the latest messages to understand the current context of the thread.

python
messages = conversation.messages.list()
for m in messages:
    print(m.get("id"), m.get("text") or m.get("json"))

Delete or leave a conversation

ouro.conversations.delete removes the conversation when you are the only member. Otherwise it removes you from the member list (leave the thread).

python
ouro.conversations.delete(conversation_id)

Comments

Comments are lightweight, rich-text notes attached to any asset (files, datasets, posts, routes, services, conversations). One-level threads are supported: top-level comments on an asset, and replies to those comments. Use the built-in Editor to compose content, then create, list, and update comments.

Create a comment

Compose with the Editor, then create a comment on a parent asset by ID.

python
parent_asset_id = "0190ea44-bfef-7f8b-9e5f-503fc20a4d91"  # can be any asset id
 
editor = ouro.comments.Editor()
editor.new_paragraph(text="Great post! I especially liked the dataset example.")
 
comment = ouro.comments.create(
    parent_id=parent_asset_id,
    content=editor,
    visibility="public",
)
print(comment.id)

List comments for an asset

Fetch all top-level comments attached to an asset.

python
comments = ouro.comments.list_by_parent(parent_asset_id)
for c in comments:
    # The content is stored as text + JSON structure
    print(c.id, c.content.text)

Retrieve a comment

python
fetched = ouro.comments.retrieve(comment.id)
print(fetched.content.text)

Update a comment

Rebuild the content with Editor or pass an updated Content object.

python
update_editor = ouro.comments.Editor()
update_editor.new_paragraph(text="Edited: adding one more note.")
 
updated_comment = ouro.comments.update(
    comment.id,
    content=update_editor,
    visibility="private",
)
print(updated_comment)

Create and list replies

Replies are simply comments whose parent is a comment. Only one level of replies is supported.

python
# Create a reply to a top-level comment
reply_editor = ouro.comments.Editor()
reply_editor.new_paragraph(text="Replying here with more details.")
 
reply = ouro.comments.create(
    parent_id=comment.id,  # parent is the comment id
    content=reply_editor,
    visibility="public",
)
 
# List replies for a top-level comment
replies = ouro.comments.list_replies(comment.id)
for r in replies:
    print(r.id, r.content.text)