Synthetic demo. Every person, company, key and provider here is fictional, and nothing leaves this server.
PromptHarbor

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

No sandbox yet.
POSTs get a fresh Idempotency-Key automatically. Use the ids from earlier responses in the path.
Response appears here.

Basics

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/persona

Returns 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

ALLOWNothing sensitive found. You still confirm the preview before it's sent.
REDACT_AND_ALLOWSensitive values are replaced with placeholders like [EMAIL_1]; you confirm the redacted preview.
REVIEW_REQUIREDAn approver who isn't you must approve first (for example confidential material).
BLOCKNot 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

401Missing, expired or tampered token
403permission_denied: wrong role, or approving your own submission
404not_found: no such item, or it belongs to another sandbox
409State conflict, for example review_required when confirming before approval
413 / 422Body too large / validation_error with details, or an idempotency key reused with a different body
429Rate limited
503unavailable: 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.