For developers and integrators

An open API and MCP server for pet screening.

REST endpoints written for non-human callers from day one. A live MCP server that lets agents drive the screening workflow with the same auth, scopes, and audit log as REST. Self-host in minutes.

A code editor showing source code in a dark theme

Quick start.

Three paths to your first integration. Pick whichever matches your stack.

# 1. Mint an API key from the admin UI, then call the API.
$ export LP_TOKEN="lp_live_abcdef123456..."

# 2. List properties you have access to.
$ curl -H "Authorization: Bearer $LP_TOKEN" \
       https://demo.leashpass.com/api/v1/properties

# 3. Create a pet for a resident.
$ curl -X POST -H "Authorization: Bearer $LP_TOKEN" \
       -H "Content-Type: application/json" \
       -H "Idempotency-Key: $(uuidgen)" \
       -d '{"name":"Buddy","species":"dog","sex":"male"}' \
       https://demo.leashpass.com/api/v1/pets

# 4. Attach a vaccination record to that pet's profile (multipart upload, 202 + status).
$ curl -X POST -H "Authorization: Bearer $LP_TOKEN" \
       -H "Idempotency-Key: $(uuidgen)" \
       -F "document_type=vaccination_record" \
       -F "file=@./rabies-2025.pdf;type=application/pdf" \
       https://demo.leashpass.com/api/v1/profiles/petprof_.../documents

Full REST reference →

# 1. Mint an API key at /admin/api-keys/new with these scopes:
#    mcp:connect, properties:read, users:read, pets:read,
#    profiles:read, profiles:review, documents:read

# 2. Add LeashPass to Claude Desktop's config:
#    ~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "leashpass": {
      "transport": "sse",
      "url": "https://demo.leashpass.com/mcp/sse",
      "headers": {
        "Authorization": "Bearer lp_live_abcdef...",
        "LeashPass-Acting-User": "agent:claude-desktop"
      }
    }
  }
}

# 3. Restart Claude Desktop and ask:
#    "What pet profiles need review at Acme East?"
#    "Approve Buddy's profile with the note 'pet rent $50/month'."
#    "Show me the vaccination record on Cooper's profile."

12 read tools (properties, residents, pets, profiles, documents) plus transition_profile and reopen_profile for state changes, update_pet / update_profile_manager_note for manager write-back, notify_resident_about_profile for templated emails, six Phase 2.0 tools for manager-initiated application requests (create_application_request, get_application_request, list_application_requests, update_application_request, submit_application_request, cancel_application_request) — the conversion on submit creates a real pet + scored pet_profile from the resident’s conversational walkthrough — attach_document_to_profile (Phase 2.1) that materialises a staged chat upload into a real document, and two Phase 2.2 preview tools (preview_transition_profile and preview_notification) that produce structured snapshots the chat UI renders as visual confirmation widgets before destructive actions fire — the action tools refuse calls without a valid preview_id with a structured permission_denied response, so the two-phase commit is enforced at the tool gate, not by prompt etiquette. Phase 2.2.2 added get_profile_workflow, the read-only sibling of preview_transition_profile — same vertical-pipeline widget, no Confirm button, no preview_id; for status-style questions like “where is Buddy in the process?” or “show me Charlie's final journey”. Phase 2.3 codified the chat response surface as a small vocabulary of structured-markdown patterns (review-queue-table, priority-callout, action-prompt, summary-card) the LLM emits directly in prose and the frontend upgrades to styled components — integrators building their own UIs can detect the same fenced-block tags to render branded components without changing the agent prompt. The inline status-pill vocabulary (๐Ÿ“ฅ submitted, ๐Ÿ” under_review, etc.) upgrades inline markdown code spans automatically. All token names + WCAG contrast notes live in docs/styling.md. Phase 2.4 added three server-rendered widget tools (list_review_queue, get_priority_item, get_profile_summary) that author the same structured payloads as Phase 2.3’s patterns — but on the server, not in the LLM. The chat handler detects each tool by name and wraps the JSON response in the canonical fenced widget block (review-queue-table / priority-callout / summary-card); the frontend renderer stays unchanged. For integrators building their own chat surface against the MCP server: call these tools the same way you’d call any other read, then either render the JSON into your own UI directly or apply the wrap pattern (detect by tool name, emit a ```<widget-tag>\n<json>\n``` fenced block in the assistant message stream, let your markdown renderer pick it up). No LLM-side JSON authoring required; the auto-scope-by-role gate keeps a profiles:read key honest about org / property / resident-on-profile boundaries. See docs/chat.md § Server-rendered visuals. Integrators building their own UIs follow the same preview-then-confirm pattern: call the preview tool, surface the snapshot to the user, then call the action tool with the returned preview_id — the preview_id lands on the success audit row's before_json so preview→confirm chains stay joinable in the audit log. Every workflow tool writes both an MCP-context audit row and a domain audit row, so the audit feed is unified across REST and MCP. Tool results include human-readable display fields so agents can surface conversational responses (“Buddy (Golden Retriever) for Maria Garcia at Acme East — under_review, score 4”) without follow-up lookups to resolve IDs into names. Full MCP reference → · Demo walkthrough →

Two surfaces, one tool catalog. External agents (Claude Desktop, customer PMS) talk to the MCP server over SSE with an API key; logged-in LeashPass users get an embedded chat at /chat with session-cookie auth. Both run through the same ExecuteTool dispatcher and emit the same mcp.tool.* audit rows; only the auth shape and transport differ. Chat backend reference →

Chat-first onboarding. Every role lands on /chat immediately after sign-in (Phase 1f.3). The page auto-fires a personalized briefing on first load: a structured system prompt tells the model to greet the user by name, confirm their role and access scope, surface 2–3 high-priority items (submitted profiles awaiting review, profiles stuck in under_review for more than a week, stale needs_info profiles, duplicate documents), and suggest one or two concrete next actions. Operators without an organization pin get a tailored variant that walks them through “Switch organization” before doing anything org-scoped. Within 5–10 seconds of clicking the demo login an evaluator sees the agent doing real work; the briefing prompt is visible in the conversation history (transparent — not a hidden trick).

Operator-tunable agent behavior. Self-hosters can shape how the chat assistant responds across the whole instance without touching code or restarting the binary (Phase 1f.4). The operator UI at /admin/site-settings edits four overlay knobs that layer onto the hardcoded base prompt as the final system-prompt sections (so operator instructions land last and therefore most-weighted):

  • Tone — formal / neutral / casual.
  • Terminology overrides — map LeashPass-default terms to your house language (residents=tenants, profile=application, …).
  • Additional instructions — up to 2000 characters appended verbatim ("Always cite the rubric version", "End approvals with 'Welcome to the building!'").
  • Site-wide policy notes — up to 5000 characters rendered as a dedicated POLICY NOTES section the model cites when relevant ("Buildings 1–4 cap at 2 pets per unit").

Save and the next chat turn reflects the change — no restart, no redeploy. Every save writes a site.settings.updated audit row with a full before/after diff. Per-org, per-user, and conversation-level overrides are deferred to a future phase; site-wide is the right granularity for a single-tenant self-hosted deployment.

Honest permission model. Every MCP write tool runs through a single Phase 1f.5 gate (internal/mcp/tools/permissions.go). When a caller is denied, the tool returns a structured payload — not a prose string — with stable fields useful for external integrators:

{
  "error_code":   "permission_denied",
  "reason":       "manager_not_assigned_to_property",
  "user_role":    "manager",
  "required":     "admin in this org, OR a manager assigned to property prop_01...",
  "alternatives": ["Sandra Chen (admin)", "Rachel Nguyen (manager assigned to this property)"],
  "message":      "permission_denied: this manager is not assigned to the property. Either an admin or one of the assigned managers needs to perform this action."
}

Surfaces: the JSON-RPC error.data field on MCP responses, the tool_result content in chat sessions, and the after_json column on the mcp.tool.<tool>.permission_denied audit row (distinct action from .failure, so operators can filter for gate hits without scraping error strings). The reason codes are stable — manager_not_assigned_to_property, operator_no_org_pin, role_cannot_write — safe to branch on in external integrations.

# 1. Run the binary. Defaults to SQLite at ./leashpass.db.
$ docker run -p 8080:8080 -v lp-data:/data \
       ghcr.io/leashpass/leashpass:latest

# 2. Bootstrap the first admin account.
$ curl -X POST http://localhost:8080/bootstrap \
       -d '{"email":"you@example.com"}'

# 3. Smoke-test the API.
$ curl http://localhost:8080/api/v1/properties \
       -H "Authorization: Bearer $LP_TOKEN"

For Postgres, set LEASHPASS_DATABASE_URL=postgres://.... Self-host guide →

Model Context Protocol, native.

MCP gives agents a structured way to call tools with rich descriptions written for AI consumption. LeashPass exposes the same operations as REST endpoints and as MCP tools, so an agent can drive the whole screening workflow without any custom integration glue.

Agent Claude / GPT / etc. LeashPass MCP server + REST API stateless app tier Postgres audit log + state SSE SQL

The app tier is stateless: any number of LeashPass processes can sit behind a load balancer without coordination. State lives in Postgres (or SQLite for single-node deployments). Sessions, idempotency, audit, and rate-limit data are all durable.

Every agent call carries a scoped API key. LeashPass-On-Behalf-Of headers record which human the agent is acting for; LeashPass-Acting-User records which agent made the call. The audit log captures both, plus IP, user-agent, the full input parameters, and the resulting state. Replay and forensics are first-class.

View the MCP tool catalog →

API design principles.

The non-negotiables that shape every endpoint.

  • Idempotency keys on every state-changing endpoint. Agents retry. Networks fail. The same Idempotency-Key header replayed within 24 hours returns the original response without re-executing the operation.
  • ULID resource IDs with semantic prefixes. pet_, petprof_, prop_, passgni_. Time-ordered, URL-safe, no collisions. The prefix tells you what you're holding without a database round-trip.
  • Strict JSON schemas with rich descriptions. Field-level constraints, enum values, and human-readable descriptions written for both humans and AI consumption.
  • RFC 9457 problem-details errors. Every error response carries type, title, status, and detail. No prose-in-200-OK error reporting.
  • Structured permission_denied on MCP write tools. When a caller is denied, the tool returns a payload with a stable reason code (manager_not_assigned_to_property, operator_no_org_pin, role_cannot_write), the caller's role, a human-readable description of what would unblock the action, and a list of named alternatives (admins and assigned managers) who can perform it. Safe to branch on in external integrations; same shape on the JSON-RPC error.data field, the chat tool_result content, and the after_json column of the audit row.
  • Capability discovery. /api/v1/discover returns the operation index, scopes, and rate-limit defaults so a new client can self-introspect without reading the docs first.
  • Deterministic responses. Same input + same state = same output. No timestamp drift, no random ordering, no non-deterministic field selection. Replay-safe.
  • Cursor pagination. Stable across writes; never produces duplicates or skips records mid-iteration.
  • Webhook-first events with HMAC signatures. State changes fire signed webhooks (Phase 1c+). HMAC-SHA256 over the canonical body, timestamp anti-replay window enforced by the receiver.
  • On-behalf-of headers. LeashPass-On-Behalf-Of and LeashPass-Acting-User let API callers attribute actions to humans for audit purposes.

Built for regulated decisioning.

When an LLM calls transition_profile to approve a screening, the audit log captures the agent's API key, the human on whose behalf the agent acted, the input parameters, and the rule-based score breakdown that justified the decision. Every step is reconstructible.

That separation — an agent that drives a workflow vs. a deterministic engine that computes the outcome — is what makes LeashPass acceptable for FHA/HUD-adjacent workflows where most LLM-driven systems are not. The score isn't whatever the LLM said; the score is what a published YAML rubric computed against the agent-supplied inputs, with the inputs themselves immutably logged.

The compliance model is documented in architecture.md and the full LP-Score design + FHA notes live in scoring.md.

LP-Score in your API responses.

Every pet_profile returned by the API now carries the rubric outcome — score, action label, classification, and the per-criterion breakdown. Use score_classification to branch on the FHA path (regular | service_animal | esa).

$ curl -s "$BASE/api/v1/profiles/petprof_01HG3K..." \
    -H "Authorization: Bearer lp_live_..." | jq '{score, score_label, score_classification}'
{
  "score": 4,
  "score_label": "Approve",
  "score_classification": "regular"
}

$ curl -s "$BASE/api/v1/profiles/petprof_01HG3K.../score-breakdown" \
    -H "Authorization: Bearer lp_live_..." | jq '.breakdown[0]'
{
  "id": "vaccinations_current",
  "description": "Vaccinations are documented and current",
  "points": 25,
  "max_points": 25,
  "rationale": "Vaccinated pet poses minimal zoonotic risk to neighbors.",
  "rule_matched": "true"
}

The MCP tool explain_profile_score returns the same shape for agents that need to answer "why was this profile scored this way?".

Try it with realistic data.

One command (or one button click in the operator UI for hosted deploys) produces an evaluation dataset: 1 organization, 2 properties, 7 staff, 15 residents, 17 pets, profiles in every workflow state, and — with --seed-logs — ~6,000 deterministic synthetic audit events spanning the last 6 months.

$ leashpass seed --demo --reset --seed-logs --yes

Hosted demos can also reset the dataset from the operator's admin dashboard at /admin/operator/seed — same seed code path, but runs in-process so files in /data/documents/ end up with correct ownership. Operator accounts persist across runs.

Connect Claude Desktop and ask:

  • "What's been the busiest property this quarter?"
  • "Show me approved profiles with scores below 70."
  • "Has Aaron Bell logged in recently? Has he reviewed anything yet?" — Aaron is the deliberate "new hire we haven't trained yet" case in the dataset; the agent finds his login events but no review actions, which is the correct answer.
  • "Are there any duplicate documents across applications?" — the dataset has one deliberate hash collision (Buddy and Bella's vaccination PDFs) so the agent has something real to report.

The dataset is fully deterministic — same flags produce byte-identical rows on every run, which makes screenshots stable and assertions easy to write. Synthetic rows carry seeded=true; the production-safety gate (LEASHPASS_PRODUCTION=true) refuses to start a real deployment that accidentally got seeded. Full docs: seeding.md.

The stack.

Boring, well-understood pieces — chosen so the binary is easy to operate and easy to read.

Go

Single static binary, fast cold-start, trivial cross-compilation. The standard library does most of what we need; the dependency tree stays small.

html/template

Server-rendered HTML with auto-escaping. No framework, no build step, no JS bundle. The whole app loads in under a second on a 3G connection.

Postgres + SQLite

Same SQL, two dialects. SQLite for dev and for single-tenant edge deployments, Postgres for everything else. Migrations and conformance tests run against both.

ONNX Runtime

For future on-device document classification. Keeps the binary fully self-hosted even when document understanding lands — no third-party vision API required.

Goose migrations

Embedded SQL migrations applied at boot. Forward-only by default; a clear path to downgrades for the rare cases that need them.

Apache 2.0

Permissive license with explicit patent grant. Suitable for commercial integration without copyleft obligations on the consumer's code.

Contributing.

LeashPass welcomes pull requests, issues, and design feedback. The CONTRIBUTING guide walks through the local development setup, test conventions, and PR review process.

All commits must carry a Developer Certificate of Origin (DCO) sign-off. The code of conduct applies to all project spaces.

Security issues should be reported via GitHub's private vulnerability reporting — please don't open public issues for vulnerabilities.