TrialCurve API documentation

One request: send an image of a Kaplan–Meier figure and get back every survival panel's digitised curves, numbers at risk, reconstructed individual patient data (Guyot 2012), hazard ratios, medians and RMST, with checks against the statistics printed on the figure.

Quickstart

Get a key from the pricing page, which includes 3 free credits every month. Then send a figure:

curl https://trialcurve.com/v1/reconstruct \
  -H "Authorization: Bearer $TRIALCURVE_KEY" \
  -F image=@figure2a.png

The response contains every arm's digitised curve, its at-risk table, reconstructed individual patient data (IPD), and summary statistics including hazard ratios. Add -F 'options={"format":"csv"}' to get IPD rows (arm,time,event) instead.

Authentication

Send your key as a bearer token: Authorization: Bearer tc_live_…. Keys are shown once after checkout. To rotate a key, call POST /v1/keys/rotate with the current key. The old key stops working immediately.

POST /v1/reconstruct

Send multipart/form-data with:

FieldTypeDescription
imagefilePNG, JPEG or WebP, up to 4 MB. A Kaplan–Meier figure, ideally including its numbers-at-risk table. Whole multi-panel figures are fine: every survival panel is returned.
optionsJSON stringOptional. See options.

Alternatively, send application/json with image_base64 plus the options fields at the top level.

Cost: 1 credit per standard request and 3 credits in audit mode. Failed requests (4xx/5xx) are not billed.

Options & hints

{
  "mode": "standard" | "audit",        // audit reads at the highest model effort (3 credits)
  "format": "json" | "csv",            // csv returns IPD rows: arm,time,event
  "overlay": false,                     // include a PNG (base64) of the reconstruction drawn over the figure
  "hints": {
    "arm_names": ["Placebo", "Drug"],  // override names, in the order the arms are returned
    "n_arms": 2,                        // if the figure has no at-risk table
    "at_risk": {                        // type the table yourself if the image doesn't include it
      "times": [0, 6, 12, 18],
      "counts": [[150, 120, 90, 60], [148, 130, 110, 91]]
    },
    "n_per_arm": [150, 148],            // no table: reconstruct from initial N (less accurate)
    "total_events": [101, 80],          // improves the final interval when reported in the paper
    "reference_arm": "Placebo",         // HR reference (name or index); default = arm with lowest RMST
    "y_percent": true                   // force the y scale if the labels are unreadable
  },
  "published": {                        // values reported in the paper, checked against the reconstruction
    "hazard_ratio": 0.72, "hr_arm": "Drug", "hr_reference": "Placebo",
    "arms": { "Drug": { "n": 148, "median": 14.2, "events": 80 } }
  }
}

Response

The top-level fields describe the first survival panel. A whole figure can hold several, so every panel is also returned in panels, each with the same fields plus what the figure printed and how the reconstruction compares with it.

{
  "id": "req_…",
  "panels": [{                                  // one entry per survival panel in the figure
    "panel": { "label": "A", "endpoint": "Overall survival", "population": "Intention-to-treat",
               "curve_source": "vision_model" | "pixel_measurement_confirmed_by_vision_model",
               "plotted": "survival" | "cumulative_incidence" },
    "arms": [ … ], "comparisons": [ … ],        // as below
    "printed_statistics": { "hazard_ratios": [ … ], "medians": [ … ], "event_counts": [ … ] },
    "printed_value_checks": [{
      "item": "hazard ratio", "printed": 0.72, "reconstructed": 0.735, "relative_difference": 0.021,
      "status": "pass" | "flag",
      "role": "validation" | "consistency",    // N and printed event counts feed the reconstruction
      "direction": "printed" | "inferred",     // was the comparison's direction stated on the figure?
      "printed_ci95": [0.60, 0.86], "within_printed_ci": true,
      "second_reading_agrees": true            // present when a second, larger model re-read a flagged figure
    }],
    "quality": { … }, "warnings": [ … ]
  }],
  "reader": { "model": "claude-opus-5-5", "effort": "high", "zoom": true, "escalated": false, "attempts": [ … ] },
  "arms": [{
    "name": "Pembrolizumab", "color": "#00bfc4",
    "curve": { "time": [0, 0.41, …], "survival": [1, 0.996, …] },   // digitised step points
    "censor_marks": [3.1, 7.9, …],
    "at_risk": { "times": [0, 12, 24, 36], "counts": [269, 144, 91, 47] },
    "ipd": { "time": [0.41, …], "event": [1, 0, …] },                // one row per reconstructed patient
    "summary": {
      "n": 269, "events": 229, "median": 14.62,
      "survival_at": { "12": 0.5412, "24": 0.3571 },
      "rmst": { "tau": 58.9, "value": 21.3 },
      "method": "guyot_2012_with_at_risk_table",
      "reconstruction_max_abs_error": 0.0071     // KM(IPD) vs digitised curve
    }
  }],
  "comparisons": [{ "arm": "Pembrolizumab", "reference": "Chemotherapy", "hazard_ratio": 0.68,
                    "ci95": [0.57, 0.81], "wald_p": 1.2e-05, "logrank_p": 1.1e-05, "rmst_difference": 5.1 }],
  "axes": { "x": { "min": 0, "max": 60, "labels_used": 6, "calibration_residual_px": 0.4, "calibration": {…} },
            "y": { "scale": "percent", … } },
  "quality": { "score": 1.0, "grade": "high", "reasons": [] },
  "warnings": [],
  "validation": { "provenance": {…}, "vision_cross_check": {…}, "published_value_checks": […], "ai_use_disclosure": {…}, "overall": {…} },
  "billing": { "units": 1 }
}

Times are in the units of the figure's time axis. Hazard ratios come from a Cox model (Efron ties) fitted to the reconstructed IPD, so they are unstratified; a printed hazard ratio that is stratified or adjusted can differ by a few percent. Cumulative-incidence panels are reconstructed on the 1 − cumulative incidence scale (panel.plotted says which). Always check quality.grade, warnings and the printed_value_checks before using the numbers.

Validation pack

In a dossier, report the method (Guyot et al. 2012 via TrialCurve, engine version), the validation status, and any discrepancies with how you resolved them.

Errors

HTTPcodeMeaning
400missing_image, bad_image, bad_optionsThe request could not be read.
401missing_api_key, invalid_api_key, revoked_api_keyCheck the Authorization header.
402subscription_inactiveThe subscription behind the key is cancelled or unpaid.
413image_too_largeImages must be under 4 MB.
422axes_not_found, x_calibration_failed, y_calibration_failed, no_curvesThe figure could not be digitised. Crop tighter or use a sharper image. Not billed.
402academic_allowance_usedAn academic key has used this month's free figures. The allowance resets on the 1st (UTC).
422no_survival_panelsNo Kaplan–Meier or cumulative-incidence panel was found in the image. Not billed.
429demo_limitDemo endpoint only.
503billing_not_configuredTemporary: the service is not accepting keys. Try again later.

Errors have the shape {"error": {"code": "…", "message": "…"}}.

Usage, keys & billing

Getting the best results

Method & citation

Reading: Claude Opus 5.5 reads the whole figure (every panel's axes, arm names, curves as step corners, numbers-at-risk table and printed statistics), with a zoom tool for small text, steps and overlapping curves. Measurement: each panel the model locates is also traced in pixels (plot-frame detection, tick calibration, colour separation, sub-pixel tracing of each step function); where the two agree, the measured curve is used. Checks: every reconstruction is compared with the hazard ratios, medians and event counts printed on the figure, and a figure that disagrees is re-read by a larger configuration. If the vision model is unavailable, the measurement engine runs alone with on-device OCR, and the response says so. Reconstruction: Guyot P, Ades AE, Ouwens MJNM, Welton NJ. Enhanced secondary analysis of survival data: reconstructing the data from published Kaplan-Meier survival curves. BMC Med Res Methodol 2012;12:9.