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.