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 lease-up detection, mode event history, and per-size fill suggestions positioned against competitor rates with floor guardrails
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/webhooksList outbound webhook subscriptions + available events
POST/api/v1/webhooksCreate subscription {url, events?} — returns the signing secret once
DELETE/api/v1/webhooks/:idDelete a subscription
POST/api/v1/webhooks/:id/testEnqueue a test ping (event recommendations.ready, test:true)
GET/api/v1/webhooks/deliveriesRecent delivery log: status, attempts, HTTP code, error
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)

Outbound webhooks

Subscribe an HTTPS URL to the recommendations.ready event and we'll POST a signed JSON payload every time new recommendations are generated for any of your facilities.

Verifying deliveries. Every POST carries X-StorIQ-Event (the event name), X-StorIQ-Delivery (delivery id), and X-StorIQ-Signature: sha256=<hex> — an HMAC-SHA256 of the raw request body keyed with your subscription secret. Compute the HMAC yourself and compare; reject anything that doesn't match. Example payload:

{
  "event": "recommendations.ready",
  "company_id": "…",
  "facility_id": "…",
  "facility_name": "Downtown",
  "recommendation_id": "…",
  "generated_at": "2026-10-01T18:00:00.000Z",
  "per_size_count": 7
}

Reliability. Deliveries retry with backoff (1 min, 5 min, 30 min, 2 h; 5 attempts max). A 2xx response marks a delivery complete; the full log (status, attempts, HTTP code, error) is in Dashboard → Webhooks and at GET /api/v1/webhooks/deliveries. The secret is shown once at creation — rotate by deleting and re-creating the subscription. You can also send a test ping from the dashboard any time.

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: Stora. Coming soon: Storeganise, storEDGE (by Storable), Storman (AU/NZ/UK), SiteLink, 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 (shipped): outbound webhooks — recommendations.ready events delivered as HMAC-signed POSTs with retry and a delivery log, documented above. Still to come: enabling the coming-soon pull adapters as their API references become available — the pull framework in lib/integrations/ is provider-agnostic.