Quickstart

Submit a tool run, wait for completion, fetch results, and download generated files.

This quickstart runs a complete API workflow: list tools, inspect a tool, submit a job, wait for completion, fetch results, and download the first generated file.

You need a paid workspace and an API key created in API keys. See Authentication for key format, scopes, and storage guidance.

Set credentials

Store your API key in an environment variable. Do not hard-code API keys in source files, notebooks that will be shared, or browser code.

Bash
export PROTEINIQ_API_KEY="pq_live_abc123_your_secret"
export PROTEINIQ_BASE_URL="https://proteiniq.io"

List tools

Use the tool catalog to find valid tool ids.

Bash
curl -s \
  -H "Authorization: Bearer $PROTEINIQ_API_KEY" \
  "$PROTEINIQ_BASE_URL/api/v1/tools"

The response is a list resource:

JSON
{
  "object": "list",
  "data": [
    {
      "id": "esmfold",
      "object": "tool",
      "name": "ESMFold",
      "category": "folding",
      "credit_model": {
        "is_free": false
      }
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Inspect a tool

Describe the tool before sending input. The detail response includes input, output, limits, processing, and credit metadata.

Bash
curl -s \
  -H "Authorization: Bearer $PROTEINIQ_API_KEY" \
  "$PROTEINIQ_BASE_URL/api/v1/tools/esmfold"

Submit a job

Submit JSON input and include an Idempotency-Key when retrying client-side requests. The same key is scoped to your workspace and prevents duplicate job creation for repeated submit requests.

Bash
curl -s -X POST \
  -H "Authorization: Bearer $PROTEINIQ_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: fold-lysozyme-001" \
  "$PROTEINIQ_BASE_URL/api/v1/jobs" \
  -d '{
    "tool": "esmfold",
    "name": "Lysozyme ESMFold run",
    "input": {
      "inputs": [
        {
          "id": "seqs_1",
          "slotId": "protein",
          "kind": "protein",
          "format": "fasta",
          "content": ">lysozyme\nKVFGRCELAAAMKRHGLDNYRGYSLGNWVCAAKFESNFNTQATNRNTDGSTDYGILQINSR",
          "source": { "type": "text" }
        }
      ]
    },
    "settings": {}
  }'

The response is a job resource. Save the id field.

JSON
{
  "id": "job_123",
  "object": "job",
  "status": "PENDING",
  "tool": "esmfold",
  "name": "Lysozyme ESMFold run",
  "credits_used": 50,
  "created_at": "2026-06-12T08:00:00.000Z",
  "started_at": null,
  "completed_at": null,
  "progress": 0,
  "execution_time_seconds": null,
  "error": null
}

Wait for completion

Poll the status endpoint until the job reaches a terminal status.

Bash
curl -s \
  -H "Authorization: Bearer $PROTEINIQ_API_KEY" \
  "$PROTEINIQ_BASE_URL/api/v1/jobs/$JOB_ID/status"

You can also stream server-sent events:

Bash
curl -N \
  -H "Authorization: Bearer $PROTEINIQ_API_KEY" \
  "$PROTEINIQ_BASE_URL/api/v1/jobs/$JOB_ID/events"

Treat COMPLETED, FAILED, TIMEOUT, CANCELLED, and BUDGET_EXCEEDED as terminal statuses. Start polling every 5 seconds, then back off to 10 seconds, 20 seconds, and a 30 second cap. If the API returns Retry-After, wait at least that many seconds before the next status or result request.

Do not replace status polling with a fixed sleep before fetching results. Job runtime and result persistence vary by tool and input size; /api/v1/results/{jobId} returns job_not_completed until the job is result-ready.

Fetch results

Fetch the result after the job is result-ready.

Bash
curl -s \
  -H "Authorization: Bearer $PROTEINIQ_API_KEY" \
  "$PROTEINIQ_BASE_URL/api/v1/results/$JOB_ID" \
  -o result.json

Download the first generated file from its signed URL:

Bash
curl -L "$(jq -r '.files[0].url' result.json)" -o result-file

Code examples

The Python example uses only the standard library. The JavaScript example uses built-in fetch in modern Node.js runtimes.

import json
import os
import random
import time
import urllib.request

base_url = os.environ.get("PROTEINIQ_BASE_URL", "https://proteiniq.io")
api_key = os.environ["PROTEINIQ_API_KEY"]

def request_json(method, path, body=None, headers=None):
    data = None if body is None else json.dumps(body).encode("utf-8")
    req = urllib.request.Request(
        f"{base_url}{path}",
        data=data,
        method=method,
        headers={
            "Authorization": f"Bearer {api_key}",
            **({"Content-Type": "application/json"} if body is not None else {}),
            **(headers or {}),
        },
    )
    with urllib.request.urlopen(req) as response:
        return json.loads(response.read().decode("utf-8")), response.headers

job, _ = request_json(
    "POST",
    "/api/v1/jobs",
    {
        "tool": "esmfold",
        "name": "Lysozyme ESMFold run",
        "input": {
            "inputs": [
                {
                    "id": "seqs_1",
                    "slotId": "protein",
                    "kind": "protein",
                    "format": "fasta",
                    "content": ">lysozyme\nKVFGRCELAAAMKRHGLDNYRGYSLGNWVCAAKFESNFNTQATNRNTDGSTDYGILQINSR",
                    "source": {"type": "text"},
                }
            ]
        },
        "settings": {},
    },
    headers={"Idempotency-Key": "fold-lysozyme-001"},
)

terminal = {"COMPLETED", "FAILED", "TIMEOUT", "CANCELLED", "BUDGET_EXCEEDED"}
poll_delay = 5
while job["status"] not in terminal:
    time.sleep(poll_delay + random.uniform(0, 1))
    job, headers = request_json("GET", f"/api/v1/jobs/{job['id']}/status")
    retry_after = int(headers.get("Retry-After") or 0)
    poll_delay = min(max(poll_delay * 2, retry_after), 30)

if job["status"] != "COMPLETED":
    raise RuntimeError(f"Job ended with status {job['status']}: {job.get('error')}")

result, _ = request_json("GET", f"/api/v1/results/{job['id']}")
print(result["files"])