Public API
Run MAIVE, WAIVE, WLS, and RTMA models from your own scripts and pipelines.
The API is free and anonymous: no accounts, no API keys, no sign-up. It is the same compute this web app uses, given a documented JSON contract. Abuse is bounded by server-side concurrency caps and edge rate limits rather than by identity.
Base URL
https://api.maive.euContent type is application/json in both directions. The machine-readable contract is the OpenAPI 3 spec, which is the source of truth; the usage guide has the same examples in narrative form.
All four methods (MAIVE, RTMA, PET-PEESE, EK) run on the same upload under the same clustering and weighting choices, in the browser and over this API alike, and every run comes with one reproducibility package.
Recipes
Four named presets; pass recipe beside data and the server resolves the rest.
A recipe expands into explicit parameters before anything you put in parameters is applied on top. Conventional PET-PEESE and EK are the non-instrumented estimators (the app's WLS model); drop shouldUseInstrumenting: false and you get the MAIVE variant of the same method instead.
| Recipe | What it runs | Expands to |
|---|---|---|
MAIVE | PET-PEESE with standard errors instrumented by sample size (Irsova et al., 2025). | {"modelType":"MAIVE","maiveMethod":"PET-PEESE","shouldUseInstrumenting":true} |
RTMA | Right-truncated meta-analysis correcting for p-hacking (Mathur, 2024; phacking package). | {"modelType":"RTMA"} |
PET-PEESE | Conventional PET-PEESE without instrumenting (the app's WLS model with the PET-PEESE method). | {"modelType":"WLS","maiveMethod":"PET-PEESE","shouldUseInstrumenting":false} |
EK | Conventional endogenous kink (EK) without instrumenting (the app's WLS model with the EK method). | {"modelType":"WLS","maiveMethod":"EK","shouldUseInstrumenting":false} |
curl -s https://api.maive.eu/v1/run-model \
-H 'Content-Type: application/json' \
-d '{
"recipe": "MAIVE",
"data": [
{"effect":0.42,"se":0.11,"n_obs":120,"study_id":"Smith2020"},
{"effect":0.31,"se":0.06,"n_obs":90,"study_id":"Smith2020"},
{"effect":0.55,"se":0.2,"n_obs":45,"study_id":"Smith2020"},
{"effect":0.12,"se":0.04,"n_obs":200,"study_id":"Jones2019"},
{"effect":0.27,"se":0.09,"n_obs":75,"study_id":"Jones2019"},
{"effect":0.18,"se":0.05,"n_obs":160,"study_id":"Jones2019"}
]
}'What ran
Every successful response says exactly which parameters were used.
One resolver fills in unset parameters for the browser and the API alike, and some of its defaults depend on the data: study clustering is on when the upload has a study_id column and the standard errors are clustered, and WLS runs with standard weights. Rather than make you re-derive that, every 200 carries resolvedParameters, the complete object the backend ran, and recipe, the named recipe it corresponds to (or null). Report it with the numbers; feed it back to reproduce them.
{
"effectEstimate": 0.2137,
"standardError": 0.0412,
"...": "...",
"resolvedParameters": {
"modelType": "WLS",
"maiveMethod": "PET-PEESE",
"weight": "standard_weights",
"standardErrorTreatment": "clustered_cr2",
"includeStudyDummies": false,
"includeStudyClustering": true,
"computeAndersonRubin": false,
"useLogFirstStage": true,
"winsorize": 0,
"shouldUseInstrumenting": false,
"favorPositive": true
},
"recipe": "PET-PEESE"
}Unknown or misspelled parameter keys and values that conflict with each other are rejected with a 400 naming the problem. The API never silently runs a different analysis than the one you asked for.
{
"error": {
"code": "validation_error",
"message": "Unknown RTMA parameter key: favourPositive. Known keys: modelType, favorPositive, alphaSelect, ciLevel, winsorize, seed."
}
}The top level of the body is checked the same way. On /v1/run-model and /v1/run-rtma every parameter, modelType included, goes inside parameters; a modelType sent beside data is a 400 that says where it belongs, not a MAIVE run. Only /v1/runs takes a top-level modelType. Any other key an endpoint does not document at the top level is rejected the same way.
{
"error": {
"code": "validation_error",
"message": "Unexpected top-level key: modelType. modelType is a run parameter and belongs inside `parameters`; this endpoint accepts data, parameters and recipe at the top level."
}
}Endpoints
| Endpoint | Kind | Description |
|---|---|---|
POST /v1/run-model | Sync | Run MAIVE, WAIVE, or WLS and get the result in the response. |
POST /v1/run-rtma | Sync | Run RTMA and get the result in the response. |
POST /v1/runs | Async | Queue a run; returns a jobId immediately. |
GET /v1/runs/{jobId} | Async | Poll a run; returns the result once it has succeeded. |
GET /v1/health | Meta | Health check. |
Synchronous or asynchronous
Async is the recommended default.
Synchronous endpoints return the result in the same response, which is the simplest thing to call, but they are subject to the edge's roughly 100 second proxy cap. That is comfortable for typical runs (about 15 to 60 seconds including a cold start) and unsuitable for anything larger.
The asynchronous path (POST /v1/runs then poll GET /v1/runs/{jobId} every 2 to 5 seconds) holds no long-lived connection, so it has no failure mode tied to run duration. Use it by default, especially for batch or CI usage. Batch status for several runs at once: GET /v1/runs?ids=a,b,c (up to 100 ids, statuses only).
Data
Send data as an array of row objects. Columns are resolved by canonical key name when present (matched case-insensitively); otherwise the first 3 or 4 keys are read positionally in the order effect, se, n_obs, study_id.
| Field | Type | Required for | Notes |
|---|---|---|---|
effect | number | MAIVE-family, RTMA | Estimated effect size. |
se | number | MAIVE-family, RTMA | Standard error; must be greater than 0, and must vary across rows for MAIVE-family runs. |
n_obs | integer | MAIVE-family | Number of observations; must be a positive integer. |
study_id | string | Optional | Enables study clustering. With study dummies on, rows must be at least unique studies plus 3. |
The MAIVE family (/v1/run-model) needs 3 or 4 columns and at least 4 rows. RTMA (/v1/run-rtma) needs 2 columns (effect, se); rows with a missing or non-positive se are silently dropped. These rules mirror the app's own validation page and run server-side, so you get a structured 400 instead of a raw R error.
Parameters
Every parameter is optional; unset ones fall back to the defaults below.
A minimal valid request is just {"data": [...]}:
{
"data": [
{"effect": 0.42, "se": 0.11, "n_obs": 120},
{"effect": 0.31, "se": 0.06, "n_obs": 90},
{"effect": 0.55, "se": 0.20, "n_obs": 45},
{"effect": 0.12, "se": 0.04, "n_obs": 200}
]
}MAIVE, WAIVE, and WLS
| Parameter | Values | Default |
|---|---|---|
modelType | MAIVE | WAIVE | WLS | MAIVE |
maiveMethod | PET | PEESE | PET-PEESE | EK | PET-PEESE |
weight | equal_weights | standard_weights | adjusted_weights | study_weights | equal_weights, or standard_weights without instrumenting |
standardErrorTreatment | not_clustered | clustered | clustered_cr2 | bootstrap | clustered_cr2 |
includeStudyDummies | boolean | false |
includeStudyClustering | boolean | derived: true when the data has study_id and SEs are clustered |
computeAndersonRubin | boolean | false |
useLogFirstStage | boolean | derived: true when instrumenting (MAIVE, WAIVE), false for WLS |
winsorize | number (percent, 0 disables) | 0 |
shouldUseInstrumenting | boolean | derived: false for WLS, true otherwise |
RTMA
| Parameter | Values | Default |
|---|---|---|
favorPositive | boolean | true |
alphaSelect | number | 0.05 |
ciLevel | number | 0.95 |
winsorize | number (percent, 0 disables) | 0 |
seed | integer (sampler seed, echoed in the response) | 2025 |
Plots (funnelPlot, zScorePlot, and their width and height companions) are excluded by default: each is a base64 PNG of roughly 50KB that most callers do not need. Add ?include=plot to any run or poll request to embed them.
RTMA convergence diagnostics
Whether an RTMA result means anything at all.
Every RTMA response carries a diagnostics object. Check it before using the numbers beside it: mu and tau are modes from an optimisation that runs separately from the sampler, so they can fail on their own while the credible intervals stay sound. A null means the value could not be read off the fit, which is not the same as the fit being healthy.
| Field | Type | Notes |
|---|---|---|
diagnostics.optimConverged | boolean | null | Whether the optimisation that produces the reported modes converged. It runs separately from the sampler, so false means mu and tau are meaningless while muCI and tauCI, being posterior quantiles, stay valid. |
diagnostics.rHat | { mu, tau }: number | null | Gelman-Rubin statistic per parameter. Above 1.01 the chains have not converged on the same distribution. |
diagnostics.nEff | { mu, tau }: number | null | Effective posterior sample size per parameter. The sampler runs four chains, so below roughly 400 the summaries rest on few independent draws. |
diagnostics.divergences | integer | null | Divergent transitions. Any at all mean part of the posterior went unexplored, so the intervals can be biased even at a healthy rHat. |
The response has no standard error for mu on purpose. The se column that phacking reports internally is the Monte Carlo error of the posterior mean, not the posterior standard deviation, and the two differ by more than an order of magnitude on ordinary data. Use muCI for uncertainty.
Example: synchronous run
curl -s https://api.maive.eu/v1/run-model \
-H 'Content-Type: application/json' \
-d '{
"data": [
{"effect": 0.42, "se": 0.11, "n_obs": 120},
{"effect": 0.31, "se": 0.06, "n_obs": 90},
{"effect": 0.55, "se": 0.20, "n_obs": 45},
{"effect": 0.12, "se": 0.04, "n_obs": 200}
]
}'Example: asynchronous run
Submit, poll until terminal, then read the result.
# 1. Submit
job=$(curl -s https://api.maive.eu/v1/runs \
-H 'Content-Type: application/json' \
-d '{
"modelType": "MAIVE",
"data": [
{"effect": 0.42, "se": 0.11, "n_obs": 120},
{"effect": 0.31, "se": 0.06, "n_obs": 90},
{"effect": 0.55, "se": 0.20, "n_obs": 45},
{"effect": 0.12, "se": 0.04, "n_obs": 200}
]
}' | jq -r '.jobId')
echo "jobId: $job"
# 2. Poll until terminal
while true; do
status=$(curl -s "https://api.maive.eu/v1/runs/$job" | tee /tmp/run.json | jq -r '.status')
echo "status: $status"
[[ "$status" == "succeeded" || "$status" == "failed" || "$status" == "timedout" ]] && break
sleep 3
done
# 3. Read the result
jq '.result' /tmp/run.jsonErrors and rate limits
Every error shares one envelope:
{
"error": {
"code": "validation_error",
"message": "Data must have 3 or 4 columns; found 6."
}
}| Code | Status | Meaning |
|---|---|---|
validation_error | 400 | The request body failed validation: bad data, an unknown or conflicting parameter, or a key the endpoint does not accept at the top level. |
not_found | 404 | Unknown or expired jobId. |
method_not_allowed | 405 | The HTTP method is not supported on this route. |
payload_too_large | 413 | The dataset is too large to queue (roughly 900KB of JSON). Use a synchronous endpoint instead. |
rate_limited | 429 | Edge rate limit or the backend concurrency cap was hit. |
internal_error | 500 | An unexpected server-side error occurred. |
not_configured | 503 | This deployment has no async runs infrastructure configured. |
Two guardrails apply: a hard concurrency cap on the compute backend, and per-IP rate limiting at the edge. Both are tuned from observed load, so treat any 429 as "retry with backoff" rather than a hard quota.
jobId and privacy
- A
jobIdis an opaque bearer token, not an account-scoped identifier. Anyone holding it can read that run's status and result, so treat it like a share link. - Runs and their results expire after 48 hours; polling after that returns
404 not_found. - Synchronous runs are stateless. Asynchronous runs persist the parameters and the result for up to 48 hours; the input dataset itself is not persisted beyond the transient queue message.
- Do not submit confidential or personally identifiable data. There is no data-classification or redaction layer; treat every request the way you would treat a request to any other unauthenticated public web service.
For AI assistants
The same contract, in the places an agent looks first.
- OpenAPI 3 spec, served by the API host: https://api.maive.eu/openapi.yaml
- llms.txt: https://easymeta.org/llms.txt
- agent.md: https://easymeta.org/agent.md
Citation
If you use this API in published or reported work, please cite the paper behind the model you ran. RTMA is Mathur's method; the other models are MAIVE's.
How to Cite This App
Please cite the method you used, and the MAIVE app that runs it, when using this tool for your research.
MAIVE, WAIVE, and WLS models
Irsova, Z., Bom, P.R.D., Havranek, T., & Rachinger, H. (2025). Spurious precision in meta-analysis of observational research. Nature Communications, 16, 8454. https://doi.org/10.1038/s41467-025-63261-0
RTMA model
Mathur, M. B. (2024). P-hacking in meta-analyses: A formalization and new meta-analytic methods. Research Synthesis Methods, 15(3), 483-499. https://doi.org/10.1002/jrsm.1701
RTMA software
Mathur, M., & Braginsky, M. (2023). phacking: Sensitivity Analysis for p-Hacking in Meta-Analyses. R package version 0.2.1. https://doi.org/10.32614/CRAN.package.phacking