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
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/sync | Idempotent nightly CRM ingest (facility + dated payload) |
| GET/POST | /api/v1/facilities | List / create facilities |
| GET/PUT/DELETE | /api/v1/facilities/:id | Read / update / delete a facility |
| POST | /api/v1/facilities/:id/upload | Ingest: JSON {rows:[...]} or text/csv (unit rates or competitor rates) |
| GET | /api/v1/facilities/:id/recommendations | Run the pricing engine; ?regenerate=0 returns the latest stored run |
| GET/POST | /api/v1/facilities/:id/competitor-rates | List / 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/portfolio | Company-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|90d | Lead 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/status | Company plan, subscription state, usage counts |
| POST | /api/v1/auth/forgot | Public: 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.