One base URL, Bearer auth, JSON in and out. Base URL: https://vibe-val.com
Every API request needs an API key in the Authorization header. Keys start with csa_ and are issued from your dashboard. The full key is shown once at creation. We store only a SHA-256 hash.
Authorization: Bearer csa_your_key_here
Revoke a key any time from the dashboard. Revoked keys fail immediately with 401 invalid_key.
POST/v1/validate
Submit a code diff and its context. The deterministic rule engine checks for the artifacts IEC 62304 requires at your risk class, then Claude writes a CSA-style rationale. The verdict comes from the rules, never the LLM.
| Field | Type | Notes |
|---|---|---|
framework | string | Must be "iec-62304" |
risk_class | string | "A", "B" or "C" |
change_type | string | "new", "modification" or "retirement" |
diff | string | The unified diff. Non-empty, max 1 MB |
context | string | What changed and why. Minimum 10 characters |
| Risk class | Cost |
|---|---|
| Class A | $0.50 |
| Class B | $2.00 |
| Class C | $5.00 |
Credits are deducted atomically before the check runs. If the balance is short, you get a 402 and nothing is charged.
200{
"id": "8f14e45f-ceea-4e4b-9a5c-9c2f0a1b2c3d",
"compliant": false,
"findings": [
{
"rule_id": "iec62304-B-test_evidence",
"severity": "major",
"message": "Missing required artifact: test evidence",
"requirement": "IEC 62304 §5.5-5.7 (Class B)"
}
],
"risk_assessment": {
"risk_class": "B",
"required_artifacts": ["change_description", "test_evidence", "design_reference"],
"missing_artifacts": ["test_evidence"]
},
"attestation": {
"timestamp": "2026-06-11T14:02:11.000Z",
"input_hash": "sha256:...",
"framework_version": "iec-62304",
"engine_version": "1.0.0"
},
"rationale_status": "ready",
"remediation": [
"Add test files or reference existing test coverage in the diff"
]
}
Finding severity is critical for Class C gaps, major for Class A and B. File attestation.input_hash in your Design History File: the same inputs always produce the same hash, so the record is independently verifiable.
GET/v1/validate/{id}/rationale
Returns the LLM-generated CSA rationale for a validation. In practice rationale_status is already "ready" when /v1/validate responds, so one call is usually enough.
| Status | Meaning |
|---|---|
200 | Rationale ready: { id, status: "ready", rationale }. If generation failed, status is "unavailable" — the verdict and attestation still stand |
202 | Still generating: { id, status: "pending", eta_seconds }. Poll again |
404 | Unknown validation ID |
GET/v1/credits/balance
{
"balance_cents": 700,
"balance_dollars": "7.00",
"cost_per_check": { "A": 50, "B": 200, "C": 500 },
"estimated_remaining": { "class_a": 14, "class_b": 3, "class_c": 1 }
}
POST/v1/checkout/credits
Creates a Stripe Checkout session for a credit pack and returns the payment URL. Most people just use the buy page, but the endpoint is there if you want to wire purchasing into your own tooling.
| Field | Notes |
|---|---|
pack | One of VIBEVAL20, VIBEVAL50, VIBEVAL100, VIBEVAL250, VIBEVAL500 |
Response: { "url": "https://checkout.stripe.com/...", "session_id": "cs_..." }. Credits land on your account when Stripe confirms payment, usually within seconds. They never expire.
The rule engine checks the diff and context for structural evidence of each artifact your risk class requires.
| Artifact | A | B | C | What the engine looks for |
|---|---|---|---|---|
change_description | ✓ | ✓ | ✓ | A meaningful context field (10+ characters) |
test_evidence | ✓ | ✓ | Test files, test directories, or test framework calls in the diff (.test.ts, describe(, assert, pytest, @Test...) | |
design_reference | ✓ | ✓ | Traceability IDs or design references (REQ-001, SRS-12, DHF, "requirement", "traceability") | |
risk_analysis | ✓ | Risk analysis markers (FMEA, "hazard", "severity", "mitigation", "risk analysis") | ||
formal_verification | ✓ | Verification evidence ("static analysis", "coverage report", MC/DC, "proof", "model check") |
Errors are JSON: { "error": "...", "code": "...", "details": "..." }.
| HTTP | Code | Meaning |
|---|---|---|
400 | invalid_body | Request body is not a JSON object |
400 | invalid_framework | Only iec-62304 is supported |
400 | invalid_risk_class | risk_class must be A, B or C |
400 | invalid_change_type | Must be new, modification or retirement |
400 | invalid_diff | diff is missing or empty |
400 | diff_too_large | diff exceeds 1 MB |
400 | invalid_context | context is missing or under 10 characters |
400 | invalid_pack | Unknown credit pack name |
401 | missing_auth | No Authorization header |
401 | invalid_key_format | Key doesn't start with csa_ |
401 | invalid_key | Key is unknown or revoked |
402 | insufficient_credits | Balance too low. Response includes cost_cents, balance_cents and buy_url |
404 | not_found | Validation ID doesn't exist |
405 | — | Wrong HTTP method for the endpoint |
502 | stripe_error | Stripe rejected the checkout request |
curl -s https://vibe-val.com/v1/validate \
-H "Authorization: Bearer $VIBEVAL_KEY" \
-H "Content-Type: application/json" \
-d '{
"framework": "iec-62304",
"risk_class": "B",
"change_type": "modification",
"diff": "--- a/src/dose.ts\n+++ b/src/dose.ts\n+ // REQ-042: clamp dose\n+ const dose = Math.min(input, MAX_DOSE);\n+ expect(clampDose(99)).toBe(MAX_DOSE);",
"context": "Clamp dose input to MAX_DOSE per REQ-042. Unit test added."
}'
import os
import requests
resp = requests.post(
"https://vibe-val.com/v1/validate",
headers={"Authorization": f"Bearer {os.environ['VIBEVAL_KEY']}"},
json={
"framework": "iec-62304",
"risk_class": "B",
"change_type": "modification",
"diff": open("change.diff").read(),
"context": "Clamp dose input to MAX_DOSE per REQ-042. Unit test added.",
},
)
result = resp.json()
if resp.status_code == 402:
raise SystemExit(f"Out of credits: {result['details']}")
resp.raise_for_status()
print("Compliant:" , result["compliant"])
print("Attestation:", result["attestation"]["input_hash"])
rationale = requests.get(
f"https://vibe-val.com/v1/validate/{result['id']}/rationale",
headers={"Authorization": f"Bearer {os.environ['VIBEVAL_KEY']}"},
).json()
print(rationale.get("rationale", "rationale unavailable"))
const BASE = "https://vibe-val.com";
const headers = {
Authorization: `Bearer ${process.env.VIBEVAL_KEY}`,
"Content-Type": "application/json",
};
const res = await fetch(`${BASE}/v1/validate`, {
method: "POST",
headers,
body: JSON.stringify({
framework: "iec-62304",
risk_class: "B",
change_type: "modification",
diff: await fs.readFile("change.diff", "utf8"),
context: "Clamp dose input to MAX_DOSE per REQ-042. Unit test added.",
}),
});
if (res.status === 402) {
const { details } = await res.json();
throw new Error(`Out of credits: ${details}`);
}
const result = await res.json();
console.log(result.compliant, result.attestation.input_hash);
const rationale = await fetch(
`${BASE}/v1/validate/${result.id}/rationale`,
{ headers },
).then((r) => r.json());
console.log(rationale.rationale ?? "rationale unavailable");
GET /v1/validate/example returns a complete fixture response, no auth required. The result viewer renders it.