API documentation v1

API-first by design: everything the dashboard does, your management software can do through /api/v1. Authenticate with a per-company API key (Dashboard → API keys):

Authorization: Bearer sk_live_...

Create keys in the dashboard — the full key is shown once; only its hash is stored. Revoke anytime. Rate limits: 120 requests/min on the API, 60/min on /sync.

Primary integration: inbound CRM sync (Phase 1)

Your CRM pushes a dated payload per facility to POST /api/v1/sync. It is an idempotent upsert keyed on facility + date — push nightly; re-sending the same date updates instead of duplicating.

Exact JSON payload schema

{
  "facility": {
    "external_id": "CRM-123",      // your CRM's facility id (preferred)
    "source_system": "sitelink",   // free-form: sitelink, storedge, api-sync, csv...
    "name": "Downtown",            // used to create/match if external_id unknown
    "address": "123 Main St",
    "website_url": "https://example.com",
    "rentable_sqft": 45200          // optional: rentable area for RevPAS $/sqft (Phase 3C)
  },
  "date": "2026-09-30",            // YYYY-MM-DD, required
  "occupancy_pct": 91.5,
  "move_ins":  { "daily": 2, "mtd": 34, "target_monthly": 40 },
  "move_outs": { "daily": 1, "mtd": 21 },
  "unit_types": [
    { "size": "5x5", "tier": "Regular", "street_rate": 89,
      "occupancy_pct": 88, "move_ins_mtd": 12 }
  ],
  "promotions": [
    { "name": "1st month 50% off", "depth_pct": 50,
      "applies_to": "new move-ins" }
  ],
  "competitor_rates": [
    { "competitor": "Acme Storage", "size": "5x5", "rate": 95,
      "promo": null, "source_url": "https://..." }
  ],
  "tenant_rents": [                       // optional: existing-tenant rents (Phase 2.6 ECRI)
    { "size": "5x5", "avg_rent": 100, "tenant_count": 12,
      "months_since_last_increase": 8, "original_rent": 90 }
  ]
}

Facility matching: facility.id (our UUID) → else external_id + source_system → else name. Unknown facilities are created automatically when a name is supplied.

Example

curl -X POST https://YOUR-APP/api/v1/sync \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"facility":{"external_id":"CRM-123","source_system":"sitelink","name":"Downtown"},
       "date":"2026-09-30","occupancy_pct":91.5,
       "move_ins":{"daily":2,"mtd":34,"target_monthly":40},
       "unit_types":[{"size":"5x5","tier":"Regular","street_rate":89}]}'
# {"ok":true,"idempotent":"created","facility":{"id":"...","name":"Downtown","created":true},...}
# Re-posting the same date returns "idempotent":"updated".

Endpoints

MethodPathDescription
POST/api/v1/syncIdempotent nightly CRM ingest (facility + dated payload)
GET/POST/api/v1/facilitiesList / create facilities
GET/PUT/DELETE/api/v1/facilities/:idRead / update / delete a facility
POST/api/v1/facilities/:id/uploadIngest: JSON {rows:[...]} or text/csv (unit rates or competitor rates)
GET/api/v1/facilities/:id/recommendationsRun the pricing engine; ?regenerate=0 returns the latest stored run
GET/POST/api/v1/facilities/:id/competitor-ratesList / add competitor rates
GET/api/v1/analytics/ecri?facility_id=Guarded existing-customer rent increase suggestions per size (Phase 2.6)
GET/api/v1/analytics/portfolioCompany-wide portfolio rollup and risk flags per facility (Phase 2.8)
GET/api/v1/analytics/seasonality?facility_id=&size=12-month demand profile from historical move-in pace, company-wide or per size, with current-month reading and soft advisory (Phase 2.9; describes past pace only)
GET/api/v1/analytics/lease-up?facility_id=Lease-up mode status per facility (or all): occupancy-driven enter (<60%) / exit (≥70%, 60–70 hysteresis), mode event history, and per-size aggressive-fill suggestions at/below competitor p25 with a 70% floor guardrail (Phase 3; practitioner thresholds)
GET/api/v1/analytics/leads?facility_id=&period=30d|90dLead source ROI & conversion funnel per facility (or all): per-channel lead counts + share of lead mix, overall lead→move-in conversion, dead-channel flags; per-channel conversion not measured — labeled honestly (Phase 3.4)
GET/api/v1/analytics/revpas?facility_id=RevPAS & effective-rate command: revenue per available unit ($/unit/mo) and per available sqft ($/sqft/mo) per facility, gross potential vs actual rent and the gap, portfolio-ranked best first, 12-month trend (Phase 3C; street-rate based, before concessions; rentable sqft from sync or estimated)
GET/api/v1/statusCompany plan, subscription state, usage counts
POST/api/v1/auth/forgotPublic: request a password-reset email (always {"ok":true}; reset link, 1-hour expiry, single use)

Errors

JSON {"error": "..."} with HTTP status: 401 bad key, 402 trial expired, 404 unknown facility, 422 validation, 429 rate limited.

Data-intake options (all three, side by side)

Option 1 · Push API — the POST /api/v1/sync flow documented above: your CRM pushes to us.

Option 2 · CSV upload — Dashboard → Facilities → Upload CSV (or POST /api/v1/facilities/:id/upload). Same normalized pipeline, templates in the upload page.

Option 3 · Connected CRM pull — Dashboard → Data sources: connect your CRM with a read-only API key and we pull from you, on demand or nightly. Live now: Storeganise, storEDGE (by Storable), and Storman (AU/NZ/UK). Coming soon: SiteLink, Storable, Tenant Inc, Yardi Breeze, Storage Commander, Easy Storage Solutions, SpaceManager, Store-It, and Monument — each is scaffolded in the product with a note explaining exactly what its API is waiting on. Use the Push API or CSV in the meantime. Credentials are encrypted at rest (AES-256-GCM) and only a masked hint is ever shown.

Roadmap

Phase 2 (planned, not built): enabling the coming-soon pull adapters as their API references become available — the pull framework in lib/integrations/ is provider-agnostic. Outbound webhooks (e.g. "new recommendations ready") are reserved under /api/v1/webhooks/* and not yet implemented.