API docs
How to test PromptHarbor from a terminal: start a sandbox, scan prompts, see what gets redacted or blocked, confirm a send, approve an exception as a manager and verify the audit chain. Generated from the app's route table, so it matches the code.
All data here is mock-up data. Each sandbox is your own isolated tenant of fictional people and companies, deleted after 2 hours. There is no real AI provider: the only destination is an in-process mock.
Quick start
# 1. Start a sandbox (you are Ada Lane, an employee)
TOKEN=$(curl -s -X POST https://promptharbor.enthernetservice.com/api/v1/demo/session | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')
# 2. Scan a prompt with contact details: REDACT_AND_ALLOW, nothing sent yet
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H "Idempotency-Key: scan-0001" \
-d '{"prompt":"Reply to jane.example@northwind.test about her late fee.","task":"customer_support","provider_id":"mock-approved","classification":"internal"}' \
https://promptharbor.enthernetservice.com/api/v1/submissions/scan
# → read "redacted_preview" and copy "id" and "preview_sha256"
# 3. Confirm exactly that preview: it goes to the mock provider with [EMAIL_1] in place
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H "Idempotency-Key: confirm-0001" \
-d '{"confirm":true,"preview_sha256":"<preview_sha256>"}' https://promptharbor.enthernetservice.com/api/v1/submissions/<id>/confirm
# 4. See what the provider actually received, then get the evidence receipt
curl -s -H "Authorization: Bearer $TOKEN" https://promptharbor.enthernetservice.com/api/v1/demo/provider-inbox
curl -s -H "Authorization: Bearer $TOKEN" https://promptharbor.enthernetservice.com/api/v1/submissions/<id>/receipt
# 5. Or run all eight frozen scenarios (A–H) one by one
curl -s https://promptharbor.enthernetservice.com/api/v1/demo/scenarios
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: run-a-0001" https://promptharbor.enthernetservice.com/api/v1/demo/scenarios/public-marketing/run
Try it
Response appears here.
Basics
- All endpoints return JSON. Errors look like
{"error": {"code", "message", "correlation_id"}}, plusdetailsfor validation errors. - Signed-in endpoints take
Authorization: Bearer <token>fromPOST /api/v1/demo/session. Your tenant always comes from the token. - Mutations marked idempotent need an
Idempotency-Keyheader (8–128 ofA–Z a–z 0–9 _ -). The same key with the same body replays the stored response (Idempotent-Replay: true); with a different body it's rejected with 422. - Requests are rate-limited per IP and per session. Prompts are capped at 8,000 characters and bodies at 64 KB.
Endpoints
Demo sandbox
POST/api/v1/demo/sessionStart an isolated synthetic sandbox; returns an Employee session
Who: any authenticated user
no token needed.
curl -s -X POST https://promptharbor.enthernetservice.com/api/v1/demo/session
{
"token": "v1.eyJleHAiOjE3OTE1\u2026",
"expires_at": "2026-10-09T11:04:16Z",
"user": {
"user_id": "usr_35aad33fc368",
"tenant_id": "sbx_906cc93ed7dc3470",
"role": "employee",
"persona": "employee",
"display_name": "Ada Lane",
"sandbox": true
}
}Every call creates a fresh, isolated sandbox tenant with synthetic data, deleted after 2 hours. Send the token as Authorization: Bearer ….
POST/api/v1/demo/session/personaSwitch to another persona in the same sandbox
Who: any authenticated user
body: persona.
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"persona":"approver"}' https://promptharbor.enthernetservice.com/api/v1/demo/session/personaReturns a new token for that persona in the same sandbox. Personas: employee (Ada Lane), employee2 (Ben Carter), approver (Mara Quinn), analyst (Sol Rivera, security analyst), admin (Pat Hughes, policy admin), auditor (Iris Novak) and owner (Theo Grant, tenant owner).
DELETE/api/v1/demo/sessionEnd the sandbox and delete all of its data
Who: any authenticated user
GET/api/v1/demo/scenariosFrozen synthetic scenarios A–H with expected outcomes
Who: any authenticated user
no token needed.
POST/api/v1/demo/scenarios/{slug}/runRun one frozen scenario end-to-end in your sandbox
Who: any authenticated user
needs an Idempotency-Key header.
POST/api/v1/demo/resetWipe this sandbox back to its initial synthetic state (naturally idempotent)
Who: any authenticated user
GET/api/v1/demo/provider-inboxWhat the mock provider actually received
Who: any authenticated user
{
"items": [
{
"submission_id": "sub_0ac88b7c3bce448ab6b5",
"provider_id": "mock-approved",
"payload": "Draft a reply to Jane Example ([EMAIL_1], [PHONE_1]) about her late fee.",
"delivered_at": "2026-10-09T09:09:03Z"
}
]
}Exactly what the mock provider received: placeholders, never the original values.
Workspace: scan, confirm, review
GET/api/v1/meCurrent user, tenant and available personas
Who: any authenticated user
POST/api/v1/submissions/scanScan a prompt and get the policy decision (nothing is sent)
Who: employee, approver
needs an Idempotency-Key header; body: prompt, task, provider_id, classification.
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: scan-0001" \
-d '{"prompt":"Draft a reply to Jane Example (jane.example@northwind.test, +1 415 555 0134) about her late fee.",
"task":"customer_support","provider_id":"mock-approved","classification":"internal"}' \
https://promptharbor.enthernetservice.com/api/v1/submissions/scan{
"id": "sub_0ac88b7c3bce448ab6b5",
"status": "AWAITING_USER_CONFIRMATION",
"decision": "REDACT_AND_ALLOW",
"hard_block": false,
"rules": {
"matched": [
"personal-data-redact"
],
"deciding": [
"personal-data-redact"
]
},
"findings": [
{
"ordinal": 0,
"detector": "email",
"category": "personal_data",
"placeholder": "[EMAIL_1]",
"fingerprint": "e6cd767cd1a196ba",
"length": 27
},
{
"ordinal": 1,
"detector": "phone",
"category": "personal_data",
"placeholder": "[PHONE_1]",
"fingerprint": "f0610c1a850d49eb",
"length": 15
}
],
"explanation": "Found: email address, phone number. Why: Contact details are replaced with placeholders before sending.",
"redacted_preview": "Draft a reply to Jane Example ([EMAIL_1], [PHONE_1]) about her late fee.",
"preview_sha256": "e2c6a72a1e000adb\u2026",
"expires_at": "2026-10-09T09:38:44Z"
}Nothing is sent by a scan. The raw prompt is never stored; findings keep only a type, a fingerprint and a length. Values: task from GET /api/v1/policies/current (for example general_writing, marketing_copy, code_assistance, customer_support, contract_summary, employment_termination_decision); provider_id mock-approved, mock-enterprise or mock-unapproved; classification public, internal, confidential or restricted.
A secret is a hard block that nobody can override:
{
"status": "BLOCKED",
"decision": "BLOCK",
"hard_block": true,
"overridable": false,
"rules": {
"matched": [
"core:secret-hard-block"
]
},
"explanation": "Found: payment API key. Why: Credentials and secrets are never sent to any provider. This rule cannot be overridden.",
"redacted_preview": "Why 401? client = payments.Client(api_key=\"[SECRET_REDACTED]\")"
}GET/api/v1/submissionsList submissions (scope=mine|tenant)
Who: any authenticated user
GET/api/v1/submissions/{id}Get one submission (preview only for submitter/approver)
Who: any authenticated user
GET/api/v1/submissions/{id}/receiptEvidence receipt: policy version, rules, finding types, digests, audit hashes
Who: any authenticated user
{
"receipt_version": 1,
"synthetic": true,
"decision": "REDACT_AND_ALLOW",
"final_status": "COMPLETED",
"policy": {
"version": 1,
"checksum": "c5f3cfb1\u2026"
},
"deciding_rule_ids": [
"personal-data-redact"
],
"finding_types": {
"email": 1,
"phone": 1
},
"content_digest": "0a152b2c\u2026",
"content_digest_alg": "HMAC-SHA256(server key, normalized prompt)",
"audit_events": [
{
"seq": 3,
"event_type": "SUBMISSION_SCANNED",
"event_hash": "4c654fd3\u2026"
}
]
}POST/api/v1/submissions/{id}/confirmExplicitly confirm and send the redacted preview to the mock provider
Who: the submitter
needs an Idempotency-Key header; body: confirm, preview_sha256.
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: confirm-0001" \
-d '{"confirm":true,"preview_sha256":"<preview_sha256 from the scan>"}' \
https://promptharbor.enthernetservice.com/api/v1/submissions/<id>/confirm{
"id": "sub_0ac88b7c3bce448ab6b5",
"status": "COMPLETED",
"decision": "REDACT_AND_ALLOW",
"delivered_at": "2026-10-09T09:09:03Z",
"provider_response": "[Synthetic mock response] Received 94 characters for task 'customer_support'. Placeholders kept intact: [EMAIL_1], [PHONE_1]. No real AI provider was contacted."
}The hash proves the user saw exactly what is sent. Confirming a REVIEW_REQUIRED submission before approval fails with review_required; a BLOCK can never be confirmed.
POST/api/v1/submissions/{id}/request-reviewAsk an approver for an exception to an overridable block
Who: the submitter
needs an Idempotency-Key header; body: reason.
POST/api/v1/submissions/{id}/feedbackReport a finding as a false positive (does not change the decision)
Who: the submitter
needs an Idempotency-Key header; body: finding_ordinal, kind.
GET/api/v1/reviewsReview queue (status=PENDING|APPROVED|REJECTED|EXPIRED|ALL)
Who: approver
curl -s -H "Authorization: Bearer $APPROVER_TOKEN" "https://promptharbor.enthernetservice.com/api/v1/reviews?status=PENDING"
Returns submissions awaiting a decision. The review id to approve is items[i].review.id (for example rev_21afc5b2c43d4370), not the submission id. An employee gets 403 permission_denied: role 'employee' cannot review.
POST/api/v1/reviews/{id}/approveApprove a pending review
Who: approver (not the submitter)
needs an Idempotency-Key header; body: reason.
curl -s -X POST -H "Authorization: Bearer $APPROVER_TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: approve-0001" \
-d '{"reason":"Enterprise tier is cleared for this contract summary."}' \
https://promptharbor.enthernetservice.com/api/v1/reviews/<review id>/approve{
"id": "sub_be56421aae6c4275ad80",
"status": "APPROVED",
"decision": "REVIEW_REQUIRED",
"review": {
"id": "rev_21afc5b2c43d4370",
"outcome": "APPROVED",
"reason": "Enterprise tier is cleared for this contract summary."
}
}The submitter then confirms to send it. Approvals expire, and the destination is re-checked against the current policy at send time.
POST/api/v1/reviews/{id}/rejectReject a pending review
Who: approver (not the submitter)
needs an Idempotency-Key header; body: reason.
Policy
GET/api/v1/policies/currentCurrent published policy (YAML, rules, providers, tasks)
Who: any authenticated user
GET/api/v1/policies/historyPolicy version history
Who: policy admin, auditor, tenant owner
POST/api/v1/policiesPublish a new policy version (validated, versioned, simulated against scenarios)
Who: policy admin
needs an Idempotency-Key header; body: yaml, change_note.
GET/api/v1/providersProvider registry from the current policy
Who: any authenticated user
POST/api/v1/tenant/explainerTurn the optional (mock) AI explainer on or off for this tenant
Who: tenant owner
needs an Idempotency-Key header; body: enabled.
Oversight
GET/api/v1/audit/eventsHash-chained audit events (no prompt content)
Who: auditor, security analyst, tenant owner
GET/api/v1/audit/verifyRe-walk and verify the tenant's audit hash chain
Who: auditor, tenant owner
{
"ok": true,
"events_checked": 9,
"head_hash": "13513fb5b93a\u2026",
"first_bad_seq": null,
"problem": null,
"scope": "hash chain integrity only; not externally anchored"
}GET/api/v1/metrics/summaryTenant workflow metrics (synthetic)
Who: security analyst, policy admin, tenant owner
Operations
GET/api/v1/healthLiveness probe
Who: any authenticated user
no token needed.
GET/api/v1/readyReadiness: database, policy and detector self-test
Who: any authenticated user
no token needed.
GET/api/v1/openapi.jsonThis API description (generated from the route table)
Who: any authenticated user
no token needed.
GET/metricsPrometheus metrics (requires PH_METRICS_TOKEN)
Who: any authenticated user
no token needed; not exposed on this server.
Decisions
| ALLOW | Nothing sensitive found. You still confirm the preview before it's sent. |
| REDACT_AND_ALLOW | Sensitive values are replaced with placeholders like [EMAIL_1]; you confirm the redacted preview. |
| REVIEW_REQUIRED | An approver who isn't you must approve first (for example confidential material). |
| BLOCK | Not sent. Some blocks can go to review on request; secrets, denied providers, data the provider isn't cleared for, and employment, credit, medical or legal decisions are hard blocks nobody can override. |
Precedence: hard BLOCK > BLOCK > REVIEW_REQUIRED > REDACT_AND_ALLOW > ALLOW. Every matching rule is recorded.
Errors
401 | Missing, expired or tampered token |
403 | permission_denied: wrong role, or approving your own submission |
404 | not_found: no such item, or it belongs to another sandbox |
409 | State conflict, for example review_required when confirming before approval |
413 / 422 | Body too large / validation_error with details, or an idempotency key reused with a different body |
429 | Rate limited |
503 | unavailable: the policy, review queue, database or audit log is down, so it fails closed and nothing is sent. A detector outage during a scan instead returns the submission with status ERROR and no decision. |