Skip to content
Docs menu

Partner API for journals

Send submissions from your editorial system and receive a score, findings and a report link for each one.

Last updated

Your system sends the code of a submission, and optionally its manuscript, and fetches the result when the analysis has finished. Authors need no RepoReady account.

Access and signing

RepoReady sets up access per journal: a partner name and a signing secret, stored encrypted. Request access at [email protected].

All endpoints live under https://api.repoready.ai/api/partner-runs. Every request carries three headers:

  • x-partner-name: your partner name.
  • x-partner-timestamp: the current Unix time in seconds.
  • x-partner-signature: the hex-encoded HMAC-SHA256 of the string below, keyed with your secret.
signature string
<x-partner-timestamp>.<METHOD>.<path>.<body>

1789650000.GET./api/partner-runs/jobs/partner_3f9c2a71d0b84e5c6a2f1b07.{}

path includes /api but not the query string. body is the JSON you send in compact form (no whitespace, keys in sent order, non-ASCII unescaped), or {} for GET. A wrong signature or a timestamp more than five minutes off returns 401.

A minimal client in Python:

client.py
import hashlib, hmac, json, time
import requests

API = "https://api.repoready.ai"
PARTNER = "your-journal"          # your partner name
SECRET = b"your-signing-secret"   # never commit it

def call(method, path, body=None):
    ts = str(int(time.time()))
    raw = json.dumps(body or {}, separators=(",", ":"), ensure_ascii=False)
    signed = f"{ts}.{method}.{path.split('?')[0]}.{raw}"
    digest = hmac.new(SECRET, signed.encode(), hashlib.sha256).hexdigest()
    headers = {
        "x-partner-name": PARTNER,
        "x-partner-timestamp": ts,
        "x-partner-signature": digest,
        "Content-Type": "application/json",
    }
    data = raw.encode() if method == "POST" else None
    r = requests.request(method, API + path, headers=headers, data=data)
    return r.json()

Submitting a job

Send the code as a zip file or a public GitHub repository.

Zip file

Ask for an upload link, PUT the file, then create the job with the returned uploadKey. The link is time-limited. Upload a manuscript the same way with "kind": "manuscript".

POST /jobs/uploads, PUT, POST /jobs
# 1. Ask for an upload link
up = call("POST", "/api/partner-runs/jobs/uploads",
          {"kind": "code", "filename": "JRNL-2026-0412.zip"})

# 2. Upload the zip file
with open("JRNL-2026-0412.zip", "rb") as f:
    requests.put(up["uploadUrl"], data=f,
                 headers={"Content-Type": "application/zip"})

# 3. Create the job
job = call("POST", "/api/partner-runs/jobs", {
    "partnerSubmissionId": "JRNL-2026-0412",
    "source": {"type": "upload", "uploadKey": up["uploadKey"]},
})
job_id = job["run"]["id"]

Public GitHub repository

Send owner/repo and a tag, branch or commit as ref; prefer a tag or commit, so the report refers to a fixed version. Private repositories are not fetched: send them as a zip file.

POST /api/partner-runs/jobs
{
  "partnerSubmissionId": "JRNL-2026-0413",
  "source": {
    "type": "github",
    "repoFullName": "example-lab/cell-atlas",
    "ref": "v1.2.0"
  },
  "manuscript": {
    "type": "upload",
    "uploadKey": "uploads/...",
    "name": "manuscript.pdf"
  },
  "effortLevel": "medium"
}

Fields

  • partnerSubmissionId (required): your ID for the submission. Resending an ID returns the existing job ("idempotent": true) instead of a new analysis; use a new ID for a revision or a retry.
  • manuscript (optional): an uploaded manuscript to compare with the code.
  • effortLevel (optional): low, medium (default) or high. Compare scores only at the same effort level.

Keep run.id from the response for the next calls.

Status and report

There are no webhooks: poll GET /jobs/:id, for example once a minute. run.status is queued, running, completed, failed or cancelled; errorMessage explains a failure.

Then fetch GET /jobs/:id/report. Check ok in every response: an unknown job ID, or a report requested too early, returns "ok": false.

GET /jobs/:id, GET /jobs/:id/report
while True:
    run = call("GET", f"/api/partner-runs/jobs/{job_id}")["run"]
    if run["status"] not in ("queued", "running"):
        break
    time.sleep(60)

if run["status"] == "completed":
    result = call("GET", f"/api/partner-runs/jobs/{job_id}/report")

What the report contains

  • score: 0 to 100, or null if part of the analysis did not complete (the summary says which). Categories follow the score ranges.
  • summary: a short summary of the assessment.
  • report.sections: findings by area (Execution, Data & Results, Code Health and, with a manuscript, Manuscript), each with name, description, status, file references (citations) and suggested fixes (suggestions).
  • report.summary: the number of findings per status.
  • reportUrl: the report page for the submission (see report links).
  • metadata: job ID, generation time and the disclaimer.
GET /api/partner-runs/jobs/:id/report
{
  "ok": true,
  "score": 64,
  "summary": "...",
  "reportUrl": "https://app.repoready.ai/shared/...",
  "report": {
    "sections": [{
      "name": "Execution",
      "checks": [{
        "name": "...",
        "description": "...",
        "status": "critical",
        "citations": ["scripts/load.py:12"],
        "suggestions": ["..."]
      }]
    }],
    "summary": { "critical": 1, "warning": 3, "improvement": 4,
                 "good": 9, "info": 2, "total": 19 }
  },
  "metadata": {
    "runId": "partner_...",
    "generatedAt": "...",
    "disclaimer": "Automated analysis may contain errors and ..."
  }
}

The status response also carries tokensUsed and estimatedCostUsd. The reproducibility checks describe what each check looks for.

Usage and limits

  • GET /usage returns your jobs by status, with tokens and estimated cost, in total or since a date (?since=2026-09-01).
  • Your access can have a limit on new jobs per 24 hours; beyond it, new jobs return 429. Resending an existing partnerSubmissionId does not count.

Report links and confidentiality

  • Jobs belong to your journal’s API access, not to a user account, and a journal can read only its own jobs.
  • RepoReady creates the report link the first time your system checks a completed job. The link contains a long random token and is not listed anywhere.
  • Report pages tell search engines not to index them.
  • Anyone with the link can open the report without signing in. Links do not expire and cannot be withdrawn through the API, so share them only with people who should see the results, such as the handling editor and reviewers.

Data handling

  • Code, manuscripts and reports are stored with our hosting provider in Germany. The analysis uses model providers listed in our privacy policy.
  • Submissions are kept until they are deleted on request; the API has no delete endpoint. Retention for your journal is agreed in its contract.
  • For personal data RepoReady processes on a journal’s behalf, both parties conclude a data processing agreement under Art. 28 GDPR (Terms § 13); template from [email protected].
  • Do not send submissions with special categories of personal data under Art. 9 GDPR, such as patient data that has not been pseudonymized.
  • Files with passwords or API keys are read like any other file; ask authors to remove and rotate committed secrets.

The services involved are described in data handling.

Displaying results

The report response includes this note as metadata.disclaimer. Show it wherever you display the score or findings:

Automated analysis may contain errors and is not a substitute for peer review.