NEXUS
APIs

MiStorage APIs

One REST API serving three audiences. Every response uses the same envelope, every protected endpoint takes a short-lived bearer token, and refresh happens through an HTTP-only cookie.

Environments

EnvironmentBase URLNotes
Local developmenthttp://localhost:5179dotnet run (http profile). The https profile adds https://localhost:7206.
Interactive reference/scalar/v1Scalar UI over the OpenAPI document at /openapi/v1.json. Development only.
ProductionAzure App ServiceReached by the MiStorage frontend; auth calls go through the frontend's same-origin relay (see below).
HealthGET /api/health on bothThe API's checks its database; the web app's (mistorage-web) only says it is up. See the Health group below.

Response envelope

Every endpoint returns ApiResponse<T>: success, a human-readable message, the payload in data, and an optional errors list. Errors use the same shape with success: false. The one exception is GET /api/health, which answers in the health-check format.

ApiResponse<T>
{
  "success": true,
  "message": "Success",
  "data": { ... },
  "errors": null
}

Authentication

Access tokens are JWTs that expire after 15 minutes and are held in browser memory only. Each audience mints its own token with a userType claim (system, tenant or renter) and is validated by a matching policy; the tables below label each endpoint with the policy it requires.

  • Send the access token as Authorization: Bearer <token>.
  • A refresh token is set as an HTTP-only cookie on login and read back by the audience's refresh endpoint. Refresh tokens are hashed at rest and rotated on every use.
  • Each audience has its own refresh-token table (RefreshTokens, TenantRefreshTokens, RenterRefreshTokens), so a cookie from one can never be presented to another.
  • Renter identity is always read from the token's sub claim, never from a route or body parameter.
Renter session policy (RenterAuth section, all keys have defaults)
KeyDefaultWhat it does
RememberMeDays30"Remember me" ticked: a persistent cookie with a rolling window that each rotation renews.
RememberMeAbsoluteDays180Ceiling on a remembered session, counted from sign-in, that no activity extends.
SessionHours12"Remember me" unticked: a session cookie, plus a server-side backstop that does not roll.
RotationGraceSeconds30How long a just-rotated token stays acceptable to the browser that rotated it — covers second-tab and reload-mid-flight races.
MaxConcurrentSessions10Live sessions per renter. At the ceiling the idlest is signed out, never the one signing in.
RevokedRetentionDays2How long a revoked row survives. While it exists a replay is recognised as theft; once swept it is merely unknown.

Rate limits

Per-IP budgets, grouped so one surface can never eat another's allowance. First matching rule wins.

BucketPathsBudget
auth/api/auth/* and /api/tenant-auth/* login, register, forgot / reset password10 / min
renter-auth/api/renter-auth/* login, register, verify, resend, forgot / reset password10 / min
rental/api/rental/start, /api/rental/quote20 / min
public/api/public/* — the anonymous storefront120 / min
security-reports/api/security/* — CSP and integrity reports60 / min

Renter auth is deliberately separate from staff auth: renters arrive in far greater numbers, and a shared allowance would let them exhaust the budget staff need to sign in. Refresh, HQ's tenant-auth/enter (there is no password to guess) and /api/health (monitors poll it) are unmetered on purpose.

The SaaS feature flag

Endpoints marked SaaS only below belong to the archived multi-tenant model. With Saas:Enabled off (the default) they answer 404 Not found to everyone, signed in or not. Three others change shape rather than disappear: POST api/tenant-auth/register becomes an administrator action, rent settles to the configured merchant regardless of per-location boarding, and GET api/dashboard/stats returns operator figures (occupancy, rent roll, arrears) instead of SaaS ones (MRR, subscriptions, churn).

Endpoint reference

Grouped by audience. Paths are relative to the API base URL; {id}-style segments are GUIDs unless noted.

Public storefront

Anonymous, read-only, GET only. Only live locations and Available units are visible. Responses are cacheable for 60 seconds. {subdomain} resolves a location by subdomain first, custom domain second.

EndpointAuthNotes
GETapi/public/{subdomain}AnonymousLocation profile plus the size guide, annotated with live availability. The one call a storefront landing page needs.
GETapi/public/{subdomain}/unitsAnonymousA page of available units. Query: category, climateControlled, sort (rate, rate_desc, size, size_desc, name), page, pageSize (default 24, max 100).
GETapi/public/{subdomain}/units/{id}AnonymousOne unit, for the detail view and the checkout's confirm step.

Renter authentication

Identity for storage customers. Accounts are scoped to one location. With RenterAuth:RequireEmailVerification on, registration returns no session and login is refused until the address is confirmed; with it off, registration signs the renter straight in.

EndpointAuthNotes
POSTapi/renter-auth/registerAnonymousCreate a renter account at a location.
POSTapi/renter-auth/verify-emailAnonymousConfirm the address with the emailed token.
POSTapi/renter-auth/resend-verificationAnonymousSend a fresh verification link.
POSTapi/renter-auth/forgot-passwordAnonymousStart password recovery. Uniform response regardless of whether the address exists.
POSTapi/renter-auth/reset-passwordAnonymousFinish password recovery with the emailed token.
POSTapi/renter-auth/loginAnonymousReturns an access token and sets the refresh cookie. rememberMe selects the persistent policy.
POSTapi/renter-auth/refreshAnonymousCookie-authenticated. Rotates the refresh token and returns a new access token.
POSTapi/renter-auth/revokeAnonymousCookie-authenticated sign-out for this session only.
POSTapi/renter-auth/revoke-allRenter tokenSign out every session for this renter.
GETapi/renter-auth/profileRenter tokenThe renter's own profile.
PUTapi/renter-auth/profileRenter tokenUpdate the renter's own profile.
GETapi/renter-auth/meRenter tokenWho am I, from the token.

Rental flow

The renter-facing half of renting a unit. Quotes are anonymous; anything touching money needs the Renter policy, and the location comes from the token.

EndpointAuthNotes
GETapi/rental/quote/{subdomain}/{unitId}AnonymousWhat this unit will cost before a card is entered. Optional startDate query. Includes which application fields the location asks for.
GETapi/rental/payment-config/{subdomain}AnonymousWhat the browser needs to tokenize a card at this location.
POSTapi/rental/startRenter tokenCharge the card (when the gateway is enabled), create the lease, take the unit off the market. First invoice is left outstanding when the gateway is off.
GETapi/rental/my-leasesRenter tokenThe signed-in renter's leases.
GETapi/rental/my-leases/{leaseId}Renter tokenOne lease in full — the account's unit page.
PUTapi/rental/my-leases/{leaseId}/move-out-noticeRenter tokenGive notice to move out. Not a cancellation: the lease stays active and rent keeps accruing until staff end it.
DELETEapi/rental/my-leases/{leaseId}/move-out-noticeRenter tokenWithdraw a notice that has not been acted on yet.
GETapi/rental/my-invoicesRenter tokenThe signed-in renter's rent invoices.

Renter account

What a signed-in renter does with their own account. Every action reads the renter id from the token.

EndpointAuthNotes
GETapi/renter-account/addressesRenter tokenThe address book.
POSTapi/renter-account/addressesRenter tokenAdd an address.
PUTapi/renter-account/addresses/{addressId}Renter tokenEdit an address.
PUTapi/renter-account/addresses/{addressId}/defaultRenter tokenMake this the default address (demotes the previous one).
DELETEapi/renter-account/addresses/{addressId}Renter tokenRemove an address.

Location staff authentication

Sign-in for a location's staff. Under the single-operator model there is no self-service registration: accounts are created by HQ or by an existing colleague.

EndpointAuthNotes
POSTapi/tenant-auth/registerStaff tokenCreate a staff account. With SaaS off this requires a token: a colleague's (the location comes from the token) or HQ's (the location comes from the body's subdomain). Returns no session. Under SaaS it is anonymous self-service. HQ normally uses the Staff tab instead.
POSTapi/tenant-auth/loginAnonymousReturns an access token and sets the refresh cookie.
POSTapi/tenant-auth/enterHQ token"Manage Facility" from HQ. Body { customerId }; HQ's bearer token yields an ordinary staff session (and refresh cookie) as the location's Owner, without a second login. The location must be active; a location older than Owner seeding gets its Owner created on first entry. The Owner's other sessions are left alone. Audited as Location.Entered.
POSTapi/tenant-auth/refreshAnonymousCookie-authenticated token rotation.
POSTapi/tenant-auth/revokeAnonymousCookie-authenticated sign-out.
GETapi/tenant-auth/meStaff tokenThe signed-in staff member and their location.
POSTapi/tenant-auth/forgot-passwordAnonymousStart password recovery.
POSTapi/tenant-auth/reset-passwordAnonymousFinish password recovery.
GETapi/tenant-auth/check-subdomainAnonymousIs this subdomain taken?
POSTapi/tenant-auth/self-registerAnonymousSaaS onlyCompany self-signup with a card.
GETapi/tenant-auth/payment-configAnonymousSaaS onlyPlatform merchant config for subscription checkout.
GETapi/tenant-auth/plan/{customerId}Staff tokenSaaS onlyThe company's current subscription plan.
POSTapi/tenant-auth/plan/{customerId}/upgradeStaff tokenSaaS onlyChange subscription plan.

Location console

Everything a location's staff do day to day. {customerId} is the location, and must match the one in the caller's token.

EndpointAuthNotes
GETapi/tenant/{customerId}/dashboardStaff tokenThe location's dashboard figures.
GETapi/tenant/{customerId}/unitsStaff tokenAll units at the location.
GETapi/tenant/{customerId}/units/size-categoriesStaff tokenThe size guide: every category with label, blurb, icon and typical dimensions.
GETapi/tenant/{customerId}/units/{id}Staff tokenOne unit.
POSTapi/tenant/{customerId}/unitsStaff tokenCreate a unit. Size category derives from the dimensions; status starts as Available. Names are unique within a location.
PUTapi/tenant/{customerId}/units/{id}Staff tokenEdit a unit: name, rate, dimensions, climate control, out-of-service, and its own application-field overrides.
PATCHapi/tenant/{customerId}/units/{id}/assignStaff tokenAssign a renter to a unit (staff-side rental).
PATCHapi/tenant/{customerId}/units/{id}/vacateStaff tokenReturn a unit to Available.
DELETEapi/tenant/{customerId}/units/{id}Staff tokenDelete a unit.
GETapi/tenant/{customerId}/rentersStaff tokenRenters at this location. Carries no application data, so a page of renters never holds everyone's licence numbers.
GETapi/tenant/{customerId}/renters/{id}Staff tokenOne renter, with the rental application from checkout in application: cell phone, driver's licence, alternate and emergency contacts, employer, referral, texting consent, hasAccount, isEmailVerified. The SSN is never returned — only hasSocialSecurityNumber. null for a renter staff created.
POSTapi/tenant/{customerId}/rentersStaff tokenCreate a renter record on the renter's behalf.
PUTapi/tenant/{customerId}/renters/{id}Staff tokenEdit a renter. Leaves the application fields untouched.
DELETEapi/tenant/{customerId}/renters/{id}Staff tokenDelete a renter.
GETapi/tenant/{customerId}/leasesStaff tokenAll leases at the location, optionally filtered by status.
POSTapi/tenant/{customerId}/leases/{leaseId}/endStaff tokenMove-out: ends the lease, returns the unit to Available, stops further rent, and only if explicitly asked writes off what is owed.
GETapi/tenant/{customerId}/settingsStaff tokenThe location's own settings: the phone number renters see, and which application fields it asks for.
PUTapi/tenant/{customerId}/settingsStaff tokenUpdate those settings. Nothing that decides money, renames or re-subdomains the location.
GETapi/tenant/{customerId}/notifications/templatesStaff tokenNotification templates.
POSTapi/tenant/{customerId}/notifications/templatesStaff tokenCreate a template.
PUTapi/tenant/{customerId}/notifications/templates/{id}Staff tokenEdit a template.
PATCHapi/tenant/{customerId}/notifications/templates/{id}/toggleStaff tokenEnable / disable a template.
DELETEapi/tenant/{customerId}/notifications/templates/{id}Staff tokenDelete a template.
POSTapi/tenant/{customerId}/notifications/recipientsStaff tokenResolve a recipient selection to renters.
POSTapi/tenant/{customerId}/notifications/previewStaff tokenRender a template against a recipient.
POSTapi/tenant/{customerId}/notifications/sendStaff tokenSend a batch.
GETapi/tenant/{customerId}/notifications/historyStaff tokenSent notifications.
GETapi/tenant/{customerId}/notifications/{id}Staff tokenOne notification.
GETapi/tenant/{customerId}/notifications/unread-countStaff tokenBadge count.

HQ authentication

Sign-in for system users behind /super-admin.

EndpointAuthNotes
POSTapi/auth/registerAnonymousCreate a system user. Open registration here is a known gap under the single-operator model and is tracked in the configuration checklist.
POSTapi/auth/loginAnonymousReturns an access token and sets the refresh cookie.
POSTapi/auth/refreshAnonymousCookie-authenticated token rotation.
POSTapi/auth/revokeAnonymousCookie-authenticated sign-out.
GETapi/auth/meHQ tokenThe signed-in user with role and permissions.
POSTapi/auth/forgot-passwordAnonymousStart password recovery.
POSTapi/auth/reset-passwordAnonymousFinish password recovery.

HQ — locations and their staff

The Customer entity, which under the single-operator model means a location. The staff endpoints sit behind the location page's Staff tab, live since 2026-09-22. None of them sends email.

EndpointAuthNotes
GETapi/customerHQ tokenList locations. Query: search, status, page, pageSize, sortBy, sortOrder.
GETapi/customer/{id}HQ tokenOne location.
POSTapi/customerHQ tokenCreate a location and seed its Owner login. One contact email may name several locations (unique only under SaaS); subdomains stay unique.
PUTapi/customer/{id}HQ tokenEdit a location.
PATCHapi/customer/{id}/statusHQ tokenChange status. Body: { status, reason }.
POSTapi/customer/{id}/banHQ tokenBan a location. Body: { reason, banUntil }.
POSTapi/customer/{id}/unbanHQ tokenLift a ban.
DELETEapi/customer/{id}HQ tokenDelete a location.
GETapi/customer/{id}/staffHQ tokenThe location's logins, Owner first: { id, firstName, lastName, email, role, isActive, createdAt, lastSeenAt }. lastSeenAt is the latest session issued, not the latest password login.
POSTapi/customer/{id}/staffHQ tokenAdd a login. Body: { firstName, lastName, email, password, confirmPassword, role? }. role is Manager (default) or Staff — never Owner. Password 8–128 characters with an uppercase letter and a digit; email unique within the location. Audited.
PATCHapi/customer/{id}/staff/{staffId}/activeHQ tokenActivate / deactivate a login. Body: { isActive }. Deactivating ends all its sessions; the Owner cannot be deactivated. Audited.
PUTapi/customer/{id}/staff/{staffId}/passwordHQ tokenSet a new password. Body: { newPassword, confirmPassword }. Signs that login out everywhere. Audited, without the password.

HQ — users, roles and permissions

System users and the RBAC behind them.

EndpointAuthNotes
GETapi/userHQ tokenList system users.
GETapi/user/{id}HQ tokenOne user.
POSTapi/userHQ tokenCreate a user.
PUTapi/user/{id}HQ tokenEdit a user.
PATCHapi/user/{id}/toggle-activeHQ tokenActivate / deactivate.
DELETEapi/user/{id}HQ tokenDelete a user.
GETapi/user/rolesHQ tokenRoles (Super Admin / Admin / Manager) and their permissions.
GETapi/user/permissionsHQ tokenAll permissions.
PUTapi/user/roles/{roleId}/permissionsHQ tokenReplace a role's permission set.

HQ — dashboard, protection plans, tasks, settings

The rest of the owner-level console.

EndpointAuthNotes
GETapi/dashboard/statsHQ tokenOperator figures: occupancy, rent roll, arrears. (Under SaaS: MRR, subscriptions, churn.)
GETapi/protectionPlansHQ tokenRenter protection plans.
GETapi/protectionPlans/{id}HQ tokenOne plan.
POSTapi/protectionPlansHQ tokenCreate a plan.
PUTapi/protectionPlans/{id}HQ tokenEdit a plan.
PATCHapi/protectionPlans/{id}/toggleHQ tokenEnable / disable.
DELETEapi/protectionPlans/{id}HQ tokenDelete a plan.
GETapi/taskHQ tokenThe HQ task board.
GETapi/task/{id}HQ tokenOne task.
POSTapi/taskHQ tokenCreate a task.
PUTapi/task/{id}HQ tokenEdit a task.
PATCHapi/task/{id}/moveHQ tokenMove a task between columns.
DELETEapi/task/{id}HQ tokenDelete a task.
GETapi/settingsHQ tokenSystem settings, including the installation-wide application-field defaults.
PUTapi/settingsHQ tokenUpdate system settings.

HQ — notifications

Templates, batches, history and replies at owner level.

EndpointAuthNotes
GETapi/notifications/templatesHQ tokenTemplates.
POSTapi/notifications/templatesHQ tokenCreate a template.
PUTapi/notifications/templates/{id}HQ tokenEdit a template.
PATCHapi/notifications/templates/{id}/toggleHQ tokenEnable / disable a template.
DELETEapi/notifications/templates/{id}HQ tokenDelete a template.
GETapi/notifications/companiesHQ tokenLocations available as recipients.
POSTapi/notifications/recipientsHQ tokenResolve a recipient selection.
POSTapi/notifications/previewHQ tokenRender a template.
POSTapi/notifications/sendHQ tokenSend a batch.
GETapi/notifications/historyHQ tokenSent notifications.
GETapi/notifications/{id}HQ tokenOne notification with its thread.
POSTapi/notifications/{id}/replyHQ tokenReply on a thread.
GETapi/notifications/unread-countHQ tokenBadge count.
PATCHapi/notifications/{id}/readHQ tokenMark as read.

HQ — payment gateway operations

Infrequent PhoenixGate admin operations, not a runtime pipeline. Readiness stays available under the single-operator model; boarding does not, because rent settles to the client's own merchant and there is nobody left to board.

EndpointAuthNotes
GETapi/payment-admin/processing-profilesHQ tokenReadiness: confirm the merchant reads transaction-ready on the gateway.
POSTapi/payment-admin/custom-fields/invoice-numberHQ tokenCreate the InvoiceNumber custom field on the merchant so every charge carries the invoice number. Safe to repeat.
POSTapi/payment-admin/boardHQ tokenSaaS onlyOne-time reseller boarding of a location's own merchant.
GETapi/payment/subscription/{subscriptionId}/autopayHQ tokenAutopay enrolment state for a subscription.
POSTapi/payment/subscription/{subscriptionId}/autopayHQ tokenEnrol / change autopay.
POSTapi/payment/reconcileHQ tokenPoll-based reconciliation against the gateway.

Archived — SaaS billing

Subscription plans, platform invoices and operator cards on file. All of these answer 404 while Saas:Enabled is false. Listed so the flag's effect is visible.

EndpointAuthNotes
GETapi/subscriptionplanAnonymousSaaS onlyPublic plan list.
GETapi/subscriptionplan/{id}HQ tokenSaaS onlyOne plan.
POSTapi/subscriptionplanHQ tokenSaaS onlyCreate a plan.
PUTapi/subscriptionplan/{id}HQ tokenSaaS onlyEdit a plan.
PATCHapi/subscriptionplan/{id}/toggleHQ tokenSaaS onlyEnable / disable.
DELETEapi/subscriptionplan/{id}HQ tokenSaaS onlyDelete a plan.
GETapi/invoiceHQ tokenSaaS onlyPlatform invoices.
GETapi/invoice/summaryHQ tokenSaaS onlyBilling summary (SQL-side aggregation).
GETapi/invoice/customer/{customerId}HQ tokenSaaS onlyInvoices for one company.
GETapi/invoice/{id}HQ tokenSaaS onlyOne invoice.
POSTapi/invoiceHQ tokenSaaS onlyCreate an invoice.
PUTapi/invoice/{id}HQ tokenSaaS onlyEdit an invoice.
POSTapi/invoice/{id}/paymentHQ tokenSaaS onlyRecord a payment.
PATCHapi/invoice/{id}/mark-overdueHQ tokenSaaS onlyMark overdue.
PATCHapi/invoice/{id}/cancelHQ tokenSaaS onlyCancel.
POSTapi/invoice/generateHQ tokenSaaS onlyGenerate the period's invoices.
POSTapi/invoice/{id}/chargeHQ tokenSaaS onlyCharge the card on file.
POSTapi/invoice/{id}/refundHQ tokenSaaS onlyRefund.
GETapi/payment-method/customer/{customerId}HQ tokenSaaS onlyA company's cards on file.
POSTapi/payment-method/customer/{customerId}HQ tokenSaaS onlyAdd a tokenized card.
PATCHapi/payment-method/customer/{customerId}/{paymentMethodId}/defaultHQ tokenSaaS onlyMake default.
DELETEapi/payment-method/customer/{customerId}/{paymentMethodId}HQ tokenSaaS onlyRemove a card.

Health

For uptime monitors. Anonymous, unthrottled, never cached, and outside the response envelope.

EndpointAuthNotes
GETapi/healthAnonymous{ "status", "service": "mistorage-api", "checks": { "database" } }: 200 when Postgres answers, 503 when it does not. Check names and statuses only — no exception text. Nothing else (email, payment gateway) is checked.
GET/api/health · web appAnonymousOn the frontend's own origin: { "status": "Healthy", "service": "mistorage-web" } with Cache-Control: no-store. Says the web app is up; it does not call the API.

Security reporting

Collection point for browser-side integrity signals on the payment pages. Anonymous by necessity; contents are a lead, never evidence. Bodies are size-capped and every field truncated before logging.

EndpointAuthNotes
POSTapi/security/csp-reportAnonymousCSP violation reports — accepts the legacy csp-report envelope and the Reporting API array.
POSTapi/security/page-integrityAnonymousPage-integrity findings from the payment pages. Logged at Error.