Documentation

Create and use an evidence profile with R via API

Create a QuantBayes evidence profile through the live API using R.

Create an evidence profile with R

This page demonstrates how to create a QuantBayes evidence profile through the live API using R.

The demo creates a private evidence profile, reads it back from the API, verifies that all rules were saved correctly, and exports the retrieved profile as reusable JSON.

What this demo tests

The script tests:

1. API key authentication
2. POST /api/v1/profiles
3. GET  /api/v1/profiles/{profileUid}
4. Rule count verification
5. Rule UID verification
6. Local JSON export of the retrieved profile

Requirements

Install the required R packages:

install.packages(c("httr", "jsonlite", "stringr"))

Create a QuantBayes API key:

1. Sign in at https://quantbayes.com/account
2. Create an API key
3. Make sure the key includes profiles:write scope
4. Save it locally

For example:

mkdir -p ~/project/data/keys
echo "qb_your_real_key_here" > ~/project/data/keys/quantbayes_api_key.txt
chmod 600 ~/project/data/keys/quantbayes_api_key.txt

Alternatively, set environment variables:

export QB_API_KEY="qb_your_real_key_here"
export QB_BASE_URL="https://quantbayes.com"

The full API key is never sent to the browser. It is used only as a bearer token in the API request.

API endpoints used

POST /api/v1/profiles
GET  /api/v1/profiles/{profileUid}

Base URL:

https://quantbayes.com

Authentication header:

Authorization: Bearer <QB_API_KEY>

Content type:

Content-Type: application/json

QEM convention

Evidence profiles use failure-check semantics.

TRUE  = problem found    = matrix 0
FALSE = evidence present = matrix 1
NA    = not available    = matrix 0

In the profile JSON, each rule defines three meanings:

evidenceMeaning      Meaning when the problem was not found
problemMeaning       Meaning when the problem was found
notAvailableMeaning  Meaning when the check cannot be assessed

Complete R demo

# quantbayes_api_create_profile_demo.R
#
# Purpose:
# Live API test for creating, reading, verifying, and exporting a QuantBayes
# evidence profile.
#
# This script tests:
#   1. API key authentication
#   2. POST /api/v1/profiles
#   3. GET  /api/v1/profiles/[profileUid]
#   4. Rule count and rule UID verification
#   5. Export of the saved profile as reusable JSON
#
# Before running:
#   1. Sign in at https://quantbayes.com/account
#   2. Create an API key
#   3. Ensure the key has profiles:write scope
#   4. Save it to:
#
#      ~/project/data/keys/quantbayes_api_key.txt
#
#   Alternatively:
#
#      export QB_API_KEY="qb_your_real_key_here"
#      export QB_BASE_URL="https://quantbayes.com"
#
# Expected result:
#   A new private profile is created, read back, verified, and saved locally
#   as JSON. The profile can be opened at:
#
#      https://quantbayes.com/profiles/[profile_uid]
#
# Security note:
#   The full API key is not printed by default. Only the prefix is printed.
#   Set PRINT_FULL_QB_API_KEY=true only for local private debugging.

suppressPackageStartupMessages({
  library(httr)
  library(jsonlite)
  library(stringr)
})

`%||%` <- function(x, y) {
  if (is.null(x) || length(x) == 0 || is.na(x) || !nzchar(as.character(x))) y else x
}

# ............................................................
# Config
# ............................................................

qb_key_path <- "~/project/data/keys/quantbayes_api_key.txt"

qb_api_key <- Sys.getenv("QB_API_KEY")
qb_base_url <- Sys.getenv("QB_BASE_URL", unset = "https://quantbayes.com")
print_full_key <- identical(Sys.getenv("PRINT_FULL_QB_API_KEY"), "true")

if (!nzchar(qb_api_key) && file.exists(path.expand(qb_key_path))) {
  qb_api_key <- readLines(qb_key_path, warn = FALSE) |>
    str_trim()
}

if (!nzchar(qb_api_key)) {
  stop("QB API key is empty. Create one at /account or set QB_API_KEY.")
}

if (!str_starts(qb_api_key, "qb_")) {
  stop("QB API key must start with qb_.")
}

if (!nzchar(qb_base_url)) {
  stop("QB base URL is empty.")
}

qb_base_url <- str_remove(qb_base_url, "/+$")

timestamp <- format(Sys.time(), "%Y%m%d_%H%M%S")
profile_uid <- paste0("api_profile_test_", timestamp)

output_dir <- file.path(getwd(), "quantbayes_api_outputs")
dir.create(output_dir, showWarnings = FALSE, recursive = TRUE)

export_json_file <- file.path(
  output_dir,
  paste0(profile_uid, ".json")
)

# ............................................................
# HTTP helper
# ............................................................

qb_request <- function(method, path, body = NULL) {
  url <- paste0(qb_base_url, path)

  args <- list(
    url = url,
    httr::add_headers(
      Authorization = paste("Bearer", qb_api_key),
      `Content-Type` = "application/json"
    )
  )

  if (!is.null(body)) {
    args$body <- jsonlite::toJSON(body, auto_unbox = TRUE, null = "null")
    args$encode <- "raw"
  }

  res <- do.call(httr::VERB, c(list(verb = method), args))
  status <- httr::status_code(res)
  raw_text <- httr::content(res, as = "text", encoding = "UTF-8")

  parsed <- tryCatch(
    jsonlite::fromJSON(raw_text, simplifyVector = FALSE),
    error = function(e) NULL
  )

  cat("\n", method, " ", path, "\n", sep = "")
  cat("HTTP status: ", status, "\n", sep = "")

  if (status < 200L || status >= 300L) {
    if (!is.null(parsed$error)) {
      cat("Error code: ", parsed$error$code %||% "NA", "\n", sep = "")
      cat("Message: ", parsed$error$message %||% "NA", "\n", sep = "")
    } else {
      cat("Raw response:\n", raw_text, "\n")
    }

    stop("QuantBayes API request failed.")
  }

  if (is.null(parsed)) {
    cat("Raw response:\n", raw_text, "\n")
    stop("Response was not valid JSON.")
  }

  if (!isTRUE(parsed$ok)) {
    print(parsed)
    stop("QuantBayes API returned ok=false.")
  }

  parsed
}

# ............................................................
# Export helper
# ............................................................

profile_response_to_builder_json <- function(profile, rules) {
  list(
    name = profile$name,
    profileUid = profile$profile_uid,
    version = profile$version,
    visibility = profile$visibility,
    domain = profile$domain,
    standard = profile$qem_standard_id,
    standardVersion = profile$qem_standard_version,
    description = profile$description %||% "",
    scope = profile$profile_json$scope %||% "",
    maintainer = profile$profile_json$maintainer %||% "",
    sourceUrl = profile$source_url %||% "",
    rules = lapply(rules, function(rule) {
      list(
        ruleOrder = rule$rule_order,
        ruleUid = rule$rule_uid,
        name = rule$name,
        description = rule$description %||% "",
        evidenceMeaning = rule$false_meaning %||% "",
        problemMeaning = rule$true_meaning %||% "",
        notAvailableMeaning = rule$na_meaning %||% ""
      )
    })
  )
}

# ............................................................
# Profile payload
# ............................................................

profile_payload <- list(
  name = "API profile creation smoke test",
  profileUid = profile_uid,
  version = "1.0",
  visibility = "private",
  domain = "general",
  standard = "SGA-QEM-1.0",
  standardVersion = "1.0",
  description = "Minimal evidence profile created through the QuantBayes API to verify profile creation.",
  scope = "API smoke testing where a small profile is created, read back, and checked for expected rule structure.",
  maintainer = "QuantBayes API test",
  sourceUrl = "",
  rules = list(
    list(
      ruleOrder = 1,
      ruleUid = "source_record_missing",
      name = "Source record missing",
      description = "Is the source record missing, inaccessible, or not inspectable?",
      evidenceMeaning = "The source record is present, accessible, and inspectable.",
      problemMeaning = "The source record is missing, inaccessible, or not inspectable.",
      notAvailableMeaning = "Source record checking is unavailable or not applicable."
    ),
    list(
      ruleOrder = 2,
      ruleUid = "evidence_trace_missing",
      name = "Evidence trace missing",
      description = "Is the evidence trace missing, incomplete, or not linked to a verifiable source?",
      evidenceMeaning = "The evidence trace is complete and linked to a verifiable source.",
      problemMeaning = "The evidence trace is missing, incomplete, or not linked to a verifiable source.",
      notAvailableMeaning = "Evidence trace checking is unavailable or not applicable."
    ),
    list(
      ruleOrder = 3,
      ruleUid = "cross_check_failed",
      name = "Cross-check failed",
      description = "Does the cross-check fail, contradict the source, or lack a recorded reconciliation?",
      evidenceMeaning = "The cross-check passes or the reconciliation is recorded.",
      problemMeaning = "The cross-check fails, contradicts the source, or lacks recorded reconciliation.",
      notAvailableMeaning = "Cross-checking is unavailable or not applicable."
    )
  )
)

expected_rule_count <- length(profile_payload$rules)
expected_rule_uids <- vapply(
  profile_payload$rules,
  function(rule) rule$ruleUid,
  character(1)
)

# ............................................................
# Run test
# ............................................................

cat("QuantBayes API profile creation test\n")
cat("Base URL: ", qb_base_url, "\n", sep = "")
cat("API key prefix: ", substr(qb_api_key, 1, 12), "...\n", sep = "")

if (print_full_key) {
  cat("API key: ", qb_api_key, "\n", sep = "")
}

cat("Profile UID: ", profile_uid, "\n", sep = "")
cat("Output directory: ", output_dir, "\n", sep = "")

create_res <- qb_request(
  "POST",
  "/api/v1/profiles",
  body = profile_payload
)

created_profile <- create_res$data$profile

if (!identical(created_profile$profile_uid, profile_uid)) {
  stop("Created profile UID does not match submitted profile UID.")
}

if (as.integer(created_profile$rule_count %||% 0L) != expected_rule_count) {
  stop("Created profile rule count does not match expected rule count.")
}

read_res <- qb_request(
  "GET",
  paste0("/api/v1/profiles/", profile_uid)
)

read_profile <- read_res$data$profile
read_rules <- read_res$data$rules

if (!identical(read_profile$profile_uid, profile_uid)) {
  stop("Read-back profile UID does not match submitted profile UID.")
}

if (length(read_rules) != expected_rule_count) {
  stop(
    paste0(
      "Read-back rule count mismatch. Expected ",
      expected_rule_count,
      " but found ",
      length(read_rules),
      "."
    )
  )
}

read_rule_uids <- vapply(read_rules, function(rule) rule$rule_uid, character(1))

missing_rules <- setdiff(expected_rule_uids, read_rule_uids)

if (length(missing_rules) > 0) {
  stop("Missing rules after read-back: ", paste(missing_rules, collapse = ", "))
}

unexpected_rules <- setdiff(read_rule_uids, expected_rule_uids)

if (length(unexpected_rules) > 0) {
  stop("Unexpected rules after read-back: ", paste(unexpected_rules, collapse = ", "))
}

# ............................................................
# Export read-back profile JSON
# ............................................................

export_profile_json <- profile_response_to_builder_json(
  profile = read_profile,
  rules = read_rules
)

writeLines(
  jsonlite::toJSON(
    export_profile_json,
    auto_unbox = TRUE,
    pretty = TRUE,
    null = "null"
  ),
  con = export_json_file
)

# ............................................................
# Result
# ............................................................

cat("\nFinal result\n")
cat("Created profile UID: ", profile_uid, "\n", sep = "")
cat("Visibility: ", created_profile$visibility, "\n", sep = "")
cat("Domain: ", created_profile$domain, "\n", sep = "")
cat("Expected rules: ", expected_rule_count, "\n", sep = "")
cat("Read-back rules: ", length(read_rules), "\n", sep = "")
cat("Exported JSON: ", export_json_file, "\n", sep = "")

cat("\nRule verification\n")
cat("Expected rule UIDs: ", paste(expected_rule_uids, collapse = ", "), "\n", sep = "")
cat("Read-back rule UIDs: ", paste(read_rule_uids, collapse = ", "), "\n", sep = "")

cat("\nLinks\n")
cat(paste0(qb_base_url, "/profiles/", profile_uid), "\n")
cat(paste0(qb_base_url, "/profiles"), "\n")

cat("\nDone.\n")

Example output

A successful run prints the API requests and a final summary.

QuantBayes API profile creation test
Base URL: https://quantbayes.com
API key prefix: qb_026479f93...
Profile UID: api_profile_test_20260514_143357
Output directory: ./quantbayes_api_outputs

POST /api/v1/profiles
HTTP status: 200

GET /api/v1/profiles/api_profile_test_20260514_143357
HTTP status: 200

Final result
Created profile UID: api_profile_test_20260514_143357
Visibility: private
Domain: general
Expected rules: 3
Read-back rules: 3
Exported JSON: ./quantbayes_api_outputs/api_profile_test_20260514_143357.json

Rule verification
Expected rule UIDs: source_record_missing, evidence_trace_missing, cross_check_failed
Read-back rule UIDs: source_record_missing, evidence_trace_missing, cross_check_failed

Links
https://quantbayes.com/profiles/api_profile_test_20260514_143357
https://quantbayes.com/profiles

Done.

What the script creates

The script creates a private profile with three rules:

source_record_missing
evidence_trace_missing
cross_check_failed

The created profile is private to the API key owner. It does not create a public repository profile.

Exported JSON

The script exports the read-back profile to:

./quantbayes_api_outputs/[profile_uid].json

The exported JSON follows the profile builder shape:

{
  "name": "API profile creation smoke test",
  "profileUid": "api_profile_test_...",
  "version": "1.0",
  "visibility": "private",
  "domain": "general",
  "standard": "SGA-QEM-1.0",
  "standardVersion": "1.0",
  "description": "...",
  "scope": "...",
  "maintainer": "QuantBayes API test",
  "sourceUrl": "",
  "rules": [
    {
      "ruleOrder": 1,
      "ruleUid": "source_record_missing",
      "name": "Source record missing",
      "description": "Is the source record missing, inaccessible, or not inspectable?",
      "evidenceMeaning": "The source record is present, accessible, and inspectable.",
      "problemMeaning": "The source record is missing, inaccessible, or not inspectable.",
      "notAvailableMeaning": "Source record checking is unavailable or not applicable."
    }
  ]
}

Common errors

Missing API key

QB API key is empty. Create one at /account or set QB_API_KEY.

Create a key at:

https://quantbayes.com/account

Then save it locally or set QB_API_KEY.

Wrong key format

QB API key must start with qb_.

Use a QuantBayes API key from the account page. Do not use a Supabase key.

Missing write scope

scope_not_allowed
API key cannot use profiles:write

Create a new API key with profiles:write scope.

Method not allowed

HTTP status: 405

The deployed site does not yet include POST /api/v1/profiles.

Interpretation boundary

This demo verifies profile creation and retrieval. It does not run QuantBayes, interpret evidence, or validate a scientific conclusion.

A profile defines the evidence checks that will later be used by QEM and QuantBayes. Results generated under a profile measure evidence sufficiency under that declared profile. They do not directly establish truth, pathogenicity, safety, regulatory acceptability, or clinical actionability.