Skip to content
Matilda TrainDocsOpen workspace
DocumentationYour research

Your research

Research HTTP API

The same owned research operations for your scripts and automation.

On this page

A versioned contract

Use https://train.matilda.com/api/v1 for programmatic research. The HTTP API and MCP tools share the same application service, ownership rules, resource policy and durable Slurm jobs. The machine-readable OpenAPI contract is available at /api/v1/openapi.json.

EndpointActionPermission
GET /profileRead account policy and runtime capabilities.research:read
POST /runs/validatePreflight a run specification and review elapsed history.research:read
POST /runsSubmit a training run.jobs:write
GET /runs and /runs/{id}Page runs and read their state.research:read
POST /runs/{id}/cancel or /resumeCancel or create a checkpoint-linked run.jobs:write
GET /runs/{id}/logs, /metrics, /artifactsRead bounded training output.research:read
GET /runs/{id}/artifacts/contentDownload an owned run artifact using a relative path.research:read
POST /allocationsReserve an interactive allocation for optional desktop access.jobs:write
GET /jobs, /jobs/{id}, /jobs/{id}/logsInspect owned native jobs, including older jobs.research:read
POST /jobs/{id}/cancelCancel an owned native job or interactive allocation.jobs:write
GET /files, /files/stat, /files/textInspect workspace files.files:read
GET /files/usageRead hard quota usage and directory estimates.files:read
GET /runs/{id}/outputsReview output inventory and manifest digest.files:read
DELETE /runs/{id}/outputsRemove reviewed terminal-run outputs and preserve history.files:write
PUT /files/text and POST /files/directoryStage source and create directories.files:write
DELETE /filesDelete a file using its reviewed expected_sha256.files:write
POST /transfersStart a resumable upload.files:write
GET /transfers/{id}Recover accepted offset and transfer phase.files:write
PUT /transfers/{id}/contentUpload a binary chunk with Upload-Offset.files:write
POST /transfers/{id}/finishVerify the full digest and publish the file.files:write
DELETE /transfers/{id}Abort an unfinished upload and release staging space.files:write
GET /files/contentDownload a file with byte ranges and If-Match.files:read

Choose a workspace and authorize its cost

GET /workspaces lists your personal and company workspaces. Choose organization_id explicitly for company funding. If omitted, your personal workspace pays. Selecting a workspace on the website does not change an agent’s paying account.

EndpointPurpose
POST /billing/quoteQuote a JobCreate resource request for its organization_id and optional fleet_id.
GET /billing/accounts/{id}Read eligible credits, reservations and costs.
GET /billing/accounts/{id}/ratesRead published rates by fleet, currency, booking and commitment.
GET /billing/accounts/{id}/statementPage allocation costs with next_cursor.
POST /billing/accounts/{id}/jobs/{job_id}/cancelStop a team-funded job with billing:write and a current owner or billing administrator role.

Submit the returned rate_card_id and maximum_micros as maximum_charge_micros together with the same organization_id, fleet_id and resources. Production requires both fields. A changed rate or an increased allocation beyond your maximum requires a fresh quote. The server rechecks membership, policy, funds and capacity before reserving credit.

A company’s financial details require billing:read plus a current owner or billing administrator role. Other research connections see their own costs. Billing permissions do not grant access to another researcher’s workspace or training output. Maincode assigns trial credits while payment setup is completed.

Matilda-backed OAuth

Use an OAuth client with the researcher resource https://train.matilda.com/mcp/research. Discover the issuer using /.well-known/oauth-protected-resource/mcp/research and the issuer metadata using /.well-known/oauth-authorization-server. Public clients use authorization code with PKCE S256 and an exactly registered return address.

The user signs in with Matilda and reviews the requested permissions. Include the resource in both authorization and token requests. The client sends its access token in the Authorization: Bearer header and rotates refresh tokens when refreshing. Do not place tokens in URLs or commit them into configuration.

Submit once. Retry safely.

OperationRequired request header
POST /runsIdempotency-Key: <UUID>
POST /runs/{id}/resumeIdempotency-Key: <UUID>
POST /allocationsIdempotency-Key: <UUID>
PUT /files/textIdempotency-Key: <UUID>
POST /transfersIdempotency-Key: <UUID>
DELETE /runs/{id}/outputsIdempotency-Key: <UUID>
PUT /transfers/{id}/contentUpload-Offset: <accepted byte count>
Quote and submit a small training run
# Save the intended run and request a funding quote first.
cat > run.json <<'RUN'
{"name":"API experiment","script":"echo hello","cpus":1,"memory_mb":4096,"minutes":5,"gpus":1,"experiment":"first-run","parameters":{},"tags":[],"inputs":[]}
RUN
curl --fail-with-body https://train.matilda.com/api/v1/billing/quote \
  -H "Authorization: Bearer $MATILDA_ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{"name":"API experiment","cpus":1,"memory_mb":4096,"minutes":5,"gpus":1}' > quote.json
# Review the cost and account in quote.json before preparing the submission.
python3 - <<'FUND'
import json
run = json.load(open('run.json'))
quote = json.load(open('quote.json'))
run.update(organization_id=quote['organization_id'],
           fleet_id=quote['fleet']['id'], rate_card_id=quote['rate_card_id'],
           maximum_charge_micros=quote['maximum_micros'])
with open('funded-run.json', 'w') as output:
    json.dump(run, output)
FUND
curl --fail-with-body https://train.matilda.com/api/v1/runs \
  -H "Authorization: Bearer $MATILDA_ACCESS_TOKEN" \
  -H "Idempotency-Key: $MATILDA_REQUEST_ID" \
  -H 'Content-Type: application/json' --data-binary @funded-run.json

MATILDA_ACCESS_TOKEN is supplied by your OAuth client. MATILDA_REQUEST_ID is a UUID created once for this intentional submission. Keep the same request ID and body after a timeout. Reuse with different arguments returns a conflict. The response is HTTP 202 with a durable run ID; poll the saved run rather than resubmitting.

Resumable binary transfers

Start a transfer with its relative path, total size, SHA-256 and an Idempotency-Key UUID. PUT binary chunks of at most 8 MiB to the returned chunk_url with Upload-Offset equal to the accepted byte count. Read transfer status after interruption and continue from that offset. POST /transfers/{id}/finish verifies and publishes the complete file.

After a timeout, read the accepted offset before retrying; the response may have been lost after the server saved your chunk. Continue from the returned offset. Retrying the previous chunk requires its original offset and identical bytes. Up to four distinct transfers can upload concurrently, with at most 10 unfinished transfers per account. Follow Retry-After on 429. Throughput depends on your connection and the gateway path; 8 MiB is a maximum, and smaller chunks can help on unstable links.

GET /files/content?path=... downloads a workspace file. An explicit Range: bytes=start-end request returns HTTP 206 and Content-Range. Keep the ETag and use If-Match when resuming so a changed file is rejected. The run-specific /runs/{id}/artifacts/content endpoint accepts a path relative to that run’s output directory.

Errors that help you recover

StatusMeaning
401Authorize or refresh a valid researcher connection.
402Eligible compute credits cannot fund the allocation; ask your billing administrator.
403Review the required scope or account access.
404The owned resource or path is unavailable.
409A version, request ID, offset or state conflicts with this operation.
410The transfer expired; start a new one.
412The downloaded file version changed.
413A chunk, inline file or storage limit was exceeded.
422Review the input fields, resource request or digest.
428Request a quote and submit its rate_card_id and maximum_charge_micros.
429 / 503Read retry guidance and retry with the same intentional request ID.

Versioned API errors include an error code, readable message, retryable flag and request_id. Send the request ID and UTC time to support if the issue persists. Paginated endpoints return next_cursor; cursor logs return next_offset. Stop polling a terminal job.

A 429 response may mean another operation is using your workspace. Follow Retry-After when supplied. Retain the same request ID and exact body when retrying a run, allocation, text write or transfer creation.

Register a public OAuth client and use PKCE

Existing MCP clients normally handle this automatically. A custom desktop client can dynamically register using POST /oauth/register with no client secret. Use an exact HTTPS callback or an HTTP loopback IP callback, such as http://127.0.0.1:40733/callback. Do not register localhost, wildcard redirects or arbitrary custom URL schemes.

Register a desktop client
umask 077
curl --fail-with-body https://train.matilda.com/oauth/register \
  -H 'Content-Type: application/json' \
  --data '{"client_name":"My research client","redirect_uris":["http://127.0.0.1:40733/callback"],"grant_types":["authorization_code","refresh_token"],"response_types":["code"],"token_endpoint_auth_method":"none"}' \
  > client-registration.json
export MATILDA_CLIENT_ID=$(python3 -c 'import json; print(json.load(open("client-registration.json"))["client_id"])')
Create a verifier and the browser authorization URL
export MATILDA_VERIFIER=$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n')
export MATILDA_STATE=$(openssl rand -hex 32)
python3 - <<'AUTHORIZE'
import base64, hashlib, os
from urllib.parse import urlencode
challenge = base64.urlsafe_b64encode(hashlib.sha256(
    os.environ["MATILDA_VERIFIER"].encode()).digest()).decode().rstrip("=")
values = {"client_id": os.environ["MATILDA_CLIENT_ID"],
    "redirect_uri": "http://127.0.0.1:40733/callback",
    "response_type": "code", "state": os.environ["MATILDA_STATE"],
    "code_challenge": challenge, "code_challenge_method": "S256",
    "resource": "https://train.matilda.com/mcp/research",
    "scope": "research:read jobs:write files:read files:write"}
print("https://train.matilda.com/oauth/authorize?" + urlencode(values))
AUTHORIZE

Have your client listen on the registered loopback callback, open the generated URL in a browser and complete consent. Before exchanging the returned code, verify that state matches the value you saved and iss equals https://train.matilda.com. Authorization attempts expire after 10 minutes; the one-use authorization code expires after 90 seconds. Never reuse a callback code.

Exchange the code and rotate a refresh token
# Your callback receiver supplies MATILDA_CODE after verifying state and iss.
curl --fail-with-body https://train.matilda.com/oauth/token \
  --data-urlencode grant_type=authorization_code \
  --data-urlencode "client_id=$MATILDA_CLIENT_ID" \
  --data-urlencode redirect_uri=http://127.0.0.1:40733/callback \
  --data-urlencode resource=https://train.matilda.com/mcp/research \
  --data-urlencode "code=$MATILDA_CODE" \
  --data-urlencode "code_verifier=$MATILDA_VERIFIER" > tokens.json
# Refresh before access expires. Read the current refresh token from secure
# storage and replace both tokens atomically only after a successful response.
curl --fail-with-body https://train.matilda.com/oauth/token \
  --data-urlencode grant_type=refresh_token \
  --data-urlencode "client_id=$MATILDA_CLIENT_ID" \
  --data-urlencode resource=https://train.matilda.com/mcp/research \
  --data-urlencode "refresh_token=$MATILDA_REFRESH_TOKEN" > tokens-next.json

Access tokens last 900 seconds (15 minutes). Refresh tokens rotate on every refresh and remain bounded by the original grant’s expiry, currently 12 hours. Keep the newest token pair securely; replaying an old refresh token revokes the connection’s token family. Requested permissions are research:read, jobs:write, files:read and files:write, with research:read required. Include the same resource in authorization, code exchange and refresh.

Exact file fields and request headers

Create a directory using JSON
curl --fail-with-body https://train.matilda.com/api/v1/files/directory \
  -H "Authorization: Bearer $MATILDA_ACCESS_TOKEN" \
  -H 'Content-Type: application/json' --data '{"path":"data"}' 

The legacy ?path=data directory query is also accepted; do not supply a different path in both places. Unknown JSON fields are rejected. PUT /files/text uses {"path":"train.py","text":"print(1)\n"} and requires Idempotency-Key: <UUID>. The field is text, not content. Include expected_sha256 only when replacing an existing reviewed file.

GET /files/text?path=train.py&offset=0&limit=32768 returns text and next_offset. Keep this byte offset for later reads. Transfer requests require Idempotency-Key when created; their binary PUT requests use Upload-Offset. Retrying accepted chunks requires the same bytes at the same offset. Whole-run cleanup uses DELETE /runs/{id}/outputs, a JSON expected_manifest_sha256 and Idempotency-Key.

EndpointPurpose
GET /files/usageHard quota usage and bounded per-directory estimates.
GET /runs/{id}/outputsReview output inventory and manifest digest.
DELETE /runs/{id}/outputsRemove reviewed outputs from a finished run, preserving its history.