07 — API Reference

Every HTTP endpoint, grouped by controller. Generated by reading every controller file in src/AssetTracking.API/Controllers/. For request/response shape, follow the linked handler in 02-backend-modules; for the underlying schema, 03-database.

Conventions

  • Base path: every controller mounts at api/v1/[controller] unless overridden by an explicit [Route("api/v1/...")] attribute. Where the kebab-case path differs from the C# class name, the override is noted in the controller's section.
  • Auth: every action carries [RequirePermission(...)] or [Authorize] or [AllowAnonymous] — the RequirePermission coverage test fails the build otherwise. The "Permission" column lists the resolved requirement (auth-only when [Authorize] with no permission key).
  • Pagination: ?page=1&pageSize=20 (defaults vary). Response shape: PagedResult<T> = { items: T[], totalCount, page, pageSize }.
  • Errors: RFC 7807 application/problem+json. Mappings detailed in 01 §Exceptions.
  • Bilingual fields: handlers accept and return *Primary / *Secondary pairs. The active UI language passes through the Accept-Language header.
  • Idempotency: only POST /audit-results is idempotent (via clientSubmissionId body field).
  • Export endpoints: list resources support GET /<resource>/export?format=xlsx|pdf returning a binary file. The format=pdf path on per-report endpoints requires the matching report.{name}.export-pdf granular permission, evaluated programmatically via EnsurePermissionAsync in ApiControllerBase.cs:25.

Controller index

Controller Base path Module
AuthController api/v1/auth Identity
UsersController api/v1/users Identity
RolesController api/v1/roles Identity
PermissionsController api/v1/permissions Identity
LoginAuditController api/v1/login-audit Identity
OrganizationsController api/v1/organizations Master Data
LocationsController api/v1/locations Master Data
ClassificationsController api/v1/classifications Master Data
VendorsController api/v1/vendors Master Data
ManufacturersController api/v1/manufacturers Master Data
AssetsController api/v1/assets Assets
AssetStatusesController api/v1/asset-statuses Assets
AssetTransfersController api/v1/asset-transfers Transfers
CheckOutsController api/v1/checkouts Custody
AuditPlansController api/v1/audit-plans Audits
AuditAssignmentsController api/v1/audit-assignments Audits
AuditResultsController api/v1/audit-results Audits
MaintenancePlansController api/v1/maintenanceplans Maintenance
MaintenanceRequestsController api/v1/maintenancerequests Maintenance
WorkOrdersController api/v1/workorders Maintenance
NotificationsController api/v1/notifications Notifications
NotificationPreferencesController api/v1/notification-preferences Notifications
NotificationTemplatesController api/v1/notification-templates Notifications
DocumentsController api/v1/documents Documents
ReportsController api/v1/reports Reporting
HierarchyConfigController api/v1/hierarchy-config Settings
AppSettingsController api/v1/app-settings Settings
TranslationsController api/v1/translations Settings
EmailProviderSettingsController api/v1/email-settings Settings
AuditLogController api/v1/audit-log System

Some controller class names do not match their kebab path exactly because the default [controller] token strips the Controller suffix and PascalCase-folds: MaintenancePlansControllermaintenanceplans (no dash unless an explicit [Route("...")] is set). The base-path column above is authoritative.


AuthController (api/v1/auth)

Method Path Permission Body Returns
POST /login [AllowAnonymous] LoginCommand { email, password, deviceId? } (IP enriched server-side) LoginResultDto { accessToken, refreshToken, expiresAt, user }
POST /refresh [AllowAnonymous] RefreshTokenCommand { refreshToken, deviceId? } (IP enriched) TokenResponse
POST /logout [Authorize] LogoutCommand { refreshToken } 204
POST /change-password [Authorize] ChangePasswordCommand { currentPassword, newPassword } 204
GET /me [Authorize] CurrentUserDto (user record)
GET /me/permissions [Authorize] string[] (effective permission keys)

UsersController (api/v1/users)

Method Path Permission Notes
GET / user.read Query: page, pageSize, search, isActive, code, email, phoneNumber, userType
GET /{id:guid} user.read
POST / user.create CreateUserCommand
PUT /{id:guid} user.update UpdateUserCommand (id from URL)
DELETE /{id:guid} user.delete Soft-delete
GET /{id:guid}/permissions user.read Effective permissions for that user
GET /{id:guid}/roles user.read List of role assignments
POST /{id:guid}/roles role.assign AssignRoleToUserCommand
DELETE /{id:guid}/roles/{roleId:guid} role.assign
GET /{id:guid}/direct-permissions user.read Paged. Query: page, pageSize, search, module, isDangerous
POST /{id:guid}/direct-permissions permission.assign-direct GrantPermissionCommand
DELETE /{id:guid}/direct-permissions/{permissionId:guid} permission.assign-direct
GET /{id:guid}/sessions user.read Query: activeOnly (default true)
DELETE /{id:guid}/sessions/{familyId:guid} user.update Revoke a refresh-token family
GET /export user.export `format=xlsx

RolesController (api/v1/roles)

Method Path Permission Notes
GET / role.read Query: page, pageSize, search, code, isBuiltIn, isSystem
GET /{id:guid} role.read
POST / role.create CreateRoleCommand
PUT /{id:guid} role.update UpdateRoleCommand
DELETE /{id:guid} role.delete Refuses if IsSystem == true
GET /{id:guid}/permissions role.read Permissions linked to the role
PUT /{id:guid}/permissions role.update AssignPermissionsToRoleCommand — diff-based; busts caches of every user holding the role
GET /export role.export

PermissionsController (api/v1/permissions)

Method Path Permission Notes
GET / permission.read Returns PermissionGroup[] grouped by Module
GET /export permission.export Flattened to one row per permission

There is no create/update/delete here — permissions are catalog-only, owned by PermissionSeeder.


LoginAuditController (api/v1/login-audit)

Method Path Permission Notes
GET / login-audit.read Query: page, pageSize, email, userId, result, from, to

OrganizationsController (api/v1/organizations)

Method Path Permission Notes
GET / organization.read Query: page, pageSize, search, parentId, rootsOnly, level, ancestorId, code
GET /children organization.read Query: parentId? (returns roots when omitted). Lazy-tree feed
GET /{id:guid} organization.read
POST / organization.create CreateOrganizationCommand
PUT /{id:guid} organization.update UpdateOrganizationCommand
DELETE /{id:guid} organization.delete
GET /template organization.read Downloads an Excel template for bulk import
GET /export organization.read Returns .xlsx
POST /import organization.import multipart/form-data file (≤ 10 MB)

LocationsController (api/v1/locations) — same shape as Organizations

Method Path Permission
GET / location.read
GET /children location.read
GET /{id:guid} location.read
POST / location.create
PUT /{id:guid} location.update
DELETE /{id:guid} location.delete
GET /template location.read
GET /export location.read
POST /import location.import

ClassificationsController (api/v1/classifications) — same shape as Organizations

Method Path Permission
GET / classification.read
GET /children classification.read
GET /{id:guid} classification.read
POST / classification.create
PUT /{id:guid} classification.update
DELETE /{id:guid} classification.delete
GET /template classification.read
GET /export classification.read
POST /import classification.import

VendorsController (api/v1/vendors)

Method Path Permission Notes
GET / vendor.read Query: page, pageSize, search, code, contactName, contactEmail, contactPhone, taxId
GET /{id:guid} vendor.read
POST / vendor.create CreateVendorCommand
PUT /{id:guid} vendor.update UpdateVendorCommand
DELETE /{id:guid} vendor.delete
GET /template vendor.read Excel template
GET /export vendor.read .xlsx
POST /import vendor.import multipart, ≤10 MB

ManufacturersController (api/v1/manufacturers) — same shape as Vendors

Method Path Permission
GET / manufacturer.read
GET /{id:guid} manufacturer.read
POST / manufacturer.create
PUT /{id:guid} manufacturer.update
DELETE /{id:guid} manufacturer.delete
GET /template manufacturer.read
GET /export manufacturer.read
POST /import manufacturer.import

AssetsController (api/v1/assets)

Method Path Permission Notes
GET / asset.read Query: page, pageSize, search, organizationId, locationId, classificationId, statusId, isCritical, includeDescendants. Returns chain arrays for tooltips.
GET /{id:guid} asset.read Includes AssetDetails
POST / asset.create CreateAssetCommand (returns FirstAsset for batch responses)
PUT /{id:guid} asset.update UpdateAssetCommand — writes history rows on dimensional changes
DELETE /{id:guid} asset.delete Soft-delete
POST /{id:guid}/status asset.update ChangeAssetStatusCommand — dedicated transition path
GET /{id:guid}/history asset.read Unified timeline (status + location + org + custody)
GET /template asset.read Excel template
GET /export asset.read .xlsx
POST /import asset.import multipart, ≤20 MB

AssetStatusesController (api/v1/asset-statuses)

Method Path Permission Notes
GET / asset.read All status rows for dropdowns

AssetTransfersController (api/v1/asset-transfers)

Method Path Permission Notes
GET / asset-transfer.read Query: page, pageSize, status, mineOnly, search, fromOrganizationId, toOrganizationId, fromLocationId, toLocationId, requestedFrom, requestedTo, includeDescendants
GET /{id:guid} asset-transfer.read
POST / asset-transfer.create Creates as Draft
POST /{id:guid}/submit asset-transfer.submit Draft → Submitted; emits transfer.pending
POST /{id:guid}/approve asset-transfer.approve Submitted → Approved → InTransit; emits transfer.approved
POST /{id:guid}/reject asset-transfer.reject Body RejectTransferCommand { rejectionReason }; emits transfer.rejected
POST /{id:guid}/lines/{lineId:guid}/receive asset-transfer.receive ReceiveTransferLineCommand { receivedStatus, notes? }
POST /{id:guid}/receive-all asset-transfer.receive ReceiveAllTransferLinesCommand
POST /{id:guid}/complete asset-transfer.complete All lines must have ReceivedAt; emits transfer.completed; writes asset histories
POST /{id:guid}/cancel asset-transfer.cancel `Draft
DELETE /{id:guid} asset-transfer.delete Soft-delete
GET /export asset-transfer.export .xlsx / .pdf

CheckOutsController (api/v1/checkouts)

Method Path Permission Notes
GET / check-out.read Query: page, pageSize, status, custodianId, assetId, mineOnly, search, checkedOutFrom, checkedOutTo
GET /{id:guid} check-out.read
POST / check-out.create CreateCheckOutCommand. Sets asset Status=OnLoan; one active check-out per asset enforced by DB index
POST /{id:guid}/check-in check-out.return CheckInCommand { returnConditionRating?, returnNotes? }; restores PreviousStatusId
POST /{id:guid}/cancel check-out.cancel Reverts asset state if active
GET /export check-out.export

AuditPlansController (api/v1/audit-plans)

Method Path Permission Notes
GET / audit-plan.read Query: page, pageSize, search, code, name, statuses[], priorities[], assignedUserIds[]
GET /{id:guid} audit-plan.read Includes Scopes
GET /{id:guid}/assignments audit-plan.read Paged list of assignments under the plan
POST / audit-plan.create CreateAuditPlanCommand
POST /preview-scope audit-plan.create Body: PreviewScopeQuery — returns the resolved asset count + sample without persisting
PUT /{id:guid} audit-plan.update UpdateAuditPlanCommand
DELETE /{id:guid} audit-plan.delete Refuses if any non-cancelled assignment exists
GET /export audit-plan.export

AuditAssignmentsController (api/v1/audit-assignments)

Method Path Permission Notes
GET / audit-assignment.read Filtered to AssignedUserId == currentUser; ordered CreatedAt DESC. Query: page, pageSize, status, search, code
GET /{id:guid} audit-assignment.read Single assignment
GET /{id:guid}/assets audit-assignment.read Snapshot rows + each asset's location chain
GET /{id:guid}/result audit-result.read The submitted result if any
POST / audit-plan.assign CreateAssignmentCommand
GET /lookup-asset/{assetId:guid} audit-assignment.submit Mobile scanner: code/name/location lookup
GET /lookup-asset-by-code audit-assignment.submit Query: code — manual entry path
GET /location-hierarchy audit-assignment.submit Levels + labels for the mobile filter
GET /{id:guid}/draft audit-assignment.submit Returns DraftPayload JSON
PUT /{id:guid}/draft audit-assignment.submit Body { payload: string } — debounced auto-save
POST /{id:guid}/photos audit-assignment.submit multipart, ≤20 MB; returns the Document id
GET /export audit-assignment.read

AuditResultsController (api/v1/audit-results)

Method Path Permission Notes
GET /pending audit-result.review Reviewer's queue. Query: page, pageSize, search, code, reviewStatus, submittedFrom, submittedTo
GET /pending/export audit-result.review .xlsx / .pdf
GET /{id:guid} audit-result.read Result + lines + review actions
GET /{id:guid}/lines audit-result.read Paged. Query: page, pageSize, search, outcome, identificationMethod, decided
POST / audit-assignment.submit SubmitResultCommandidempotent via clientSubmissionId; emits audit.result.review-required
POST /{id:guid}/review audit-result.review ReviewLinesCommand (single-line). On full-coverage approve: assignment → Completed; emits audit.result.approved
POST /{id:guid}/bulk-review audit-result.review BulkReviewLinesCommand (multi-line)
GET /photo/{documentId:guid}/content audit-result.read OR audit-assignment.submit Streams a photo. Either reviewer (post-submit) or auditor (pre-submit) — handler enforces auditor-only path for unlinked photos
GET /excel-template/{assignmentId:guid} audit-assignment.submit Builds a pre-filled .xlsx for the offline path
POST /from-excel audit-assignment.submit multipart, ≤20 MB. Form: auditAssignmentId, clientSubmissionId, deviceInfo? + the file

MaintenancePlansController (api/v1/maintenanceplans)

Method Path Permission Notes
GET / maintenance-plan.read Query: page, pageSize, assetId, isActive, search, code, frequency, nextDueFrom, nextDueTo
GET /{id:guid} maintenance-plan.read Includes Assets join collection
GET /{id:guid}/workorders maintenance-plan.read Paged WO history per plan
POST / maintenance-plan.create CreatePlanCommand — requires assetIds[] (≥1)
PUT /{id:guid} maintenance-plan.update Diffs join rows
DELETE /{id:guid} maintenance-plan.delete Soft-delete (cascades the join)
POST /{id:guid}/generate-work-orders maintenance-plan.generate One WO per plan-asset; refuses if NextDueDate > now. Rolls plan: LastPerformedDate=now, NextDueDate += frequency
GET /export maintenance-plan.export

MaintenanceRequestsController (api/v1/maintenancerequests)

Method Path Permission Notes
GET / maintenance-request.read Query: page, pageSize, status, severity, assetId, mineOnly, search, code, reportedFrom, reportedTo
GET /{id:guid} maintenance-request.read
POST / maintenance-request.create CreateRequestCommand
POST /{id:guid}/promote maintenance-request.review PromoteToWorkOrderCommand
POST /{id:guid}/reject maintenance-request.review RejectRequestCommand { rejectionReason }
GET /export maintenance-request.export

WorkOrdersController (api/v1/workorders)

Method Path Permission Notes
GET / work-order.read Query: page, pageSize, status, type, assetId, assignedToUserId, mineOnly, search, code, priority, scheduledFrom, scheduledTo
GET /{id:guid} work-order.read
POST / work-order.create CreateWorkOrderCommand
POST /{id:guid}/assign work-order.assign AssignWorkOrderCommand (user XOR vendor); emits work-order.assigned
POST /{id:guid}/start work-order.update Assigned → InProgress; sets Asset.Status=InMaintenance, captures previous
POST /{id:guid}/complete work-order.close CompleteWorkOrderCommand { actualEnd?, downtimeHours?, laborCost?, partsCost?, currencyCode?, resolutionNotes? }; restores asset previous status
POST /{id:guid}/cancel work-order.update
GET /export work-order.export

NotificationsController (api/v1/notifications)

Method Path Permission Notes
GET / [Authorize] Query: page, pageSize, unreadOnly
GET /unread-count [Authorize] Returns { unreadCount }
POST /{id:guid}/read [Authorize]
POST /read-all [Authorize]
DELETE /{id:guid} [Authorize]
DELETE / [Authorize] Query: onlyRead (default false); returns { removed: int }

Notifications are scoped to the caller — RecipientUserId == currentUser — so [Authorize] alone is sufficient.

NotificationPreferencesController (api/v1/notification-preferences)

Method Path Permission Notes
GET / [Authorize] The caller's preferences
PUT / [Authorize] UpdateMyPreferencesCommand

NotificationTemplatesController (api/v1/notification-templates)

Method Path Permission Notes
GET / notification-template.read All templates
GET /{id:guid} notification-template.read
POST / notification-template.update CreateTemplateCommand
PUT /{id:guid} notification-template.update UpdateTemplateCommand
DELETE /{id:guid} notification-template.delete
GET /events notification-template.read The NotificationEventCatalog
POST /preview notification-template.read PreviewTemplateCommand — render a draft with sample merge fields
GET /available [Authorize] Identifiers + subjects only — for the user's preferences page

DocumentsController (api/v1/documents)

Method Path Permission Notes
GET /{id:guid} document.read Metadata
GET /by-owner document.read Query: ownerType, ownerId
GET /{id:guid}/content document.read Streams the bytes
POST / document.upload multipart, ≤100 MB. Form: file, category?, nameEn?, nameAr?, ownerType?, ownerId? (auto-link if provided)
DELETE /{id:guid} document.delete
POST /{id:guid}/links document.upload LinkDocumentCommand
DELETE /links/{linkId:guid} document.delete Unlink (document remains)

ReportsController (api/v1/reports)

Six business reports, each with a paged-list and an export endpoint. Export endpoints check report.{key}.read first, then programmatically gate by format on report.{key}.export-excel vs report.{key}.export-pdf.

Asset Inventory (/asset-inventory)

Method Path Permission
GET /asset-inventory report.asset-inventory.read
GET /asset-inventory/export report.asset-inventory.read AND (export-excel or export-pdf)

Query: page, pageSize, search, organizationId, locationId, classificationId, statusId, isCritical, includeDescendants.

Audit Results (/audit-results)

Method Path Permission
GET /audit-results report.audit-results.read
GET /audit-results/export report.audit-results.read + format gate

Query: search, planId, assetId, outcome, reviewStatus, submittedFrom, submittedTo.

Audit History (/audit-history)

Method Path Permission
GET /audit-history report.audit-history.read
GET /audit-history/export report.audit-history.read + format gate

Query: search, assetId, planId, outcome, reviewStatus. (dueFrom/dueTo filters were removed when DueDate left the system.)

Maintenance History (/maintenance-history)

Method Path Permission
GET /maintenance-history report.maintenance-history.read
GET /maintenance-history/export report.maintenance-history.read + format gate

Query: search, assetId, vendorId, type, priority, status, completedFrom, completedTo.

Transfer History (/transfer-history)

Method Path Permission
GET /transfer-history report.transfer-history.read
GET /transfer-history/export report.transfer-history.read + format gate

Query: search, assetId, fromOrganizationId, toOrganizationId, fromLocationId, toLocationId, status, requestedFrom, requestedTo.

Checkout Activity (/checkout-activity)

Method Path Permission
GET /checkout-activity report.checkout-activity.read
GET /checkout-activity/export report.checkout-activity.read + format gate

Query: search, assetId, custodianId, status, overdueOnly, checkedOutFrom, checkedOutTo.

HierarchyConfigController (api/v1/hierarchy-config)

Method Path Permission Notes
GET /{entityType} hierarchy-config.read entityType = Location / Organization / Classification
PUT /{entityType} hierarchy-config.update SetHierarchyConfigCommand

AppSettingsController (api/v1/app-settings)

Method Path Permission Notes
GET / [Authorize] Singleton; primary/secondary language codes
PUT / app-settings.update UpdateAppSettingsCommand

TranslationsController (api/v1/translations)

Method Path Permission Notes
GET / translation.read All override rows
GET /active/{language} [Authorize] Resolved key→value map for one language
PUT /{language}/{key} translation.update Body { value }
DELETE /{language}/{key} translation.update Removes the override (resets to bundled default)
GET /export translation.export

EmailProviderSettingsController (api/v1/email-settings)

Method Path Permission Notes
GET / email-settings.manage Singleton
PUT / email-settings.manage UpdateEmailProviderSettingsCommand (encrypts password)
POST /test email-settings.manage TestEmailProviderSettingsCommand { toAddress } — sends a one-off; always returns 200 with { success, message }

AuditLogController (api/v1/audit-log)

Method Path Permission Notes
GET / audit-log.read Query: page, pageSize, entityName, entityId, userId, action, from, to
GET /entity-names audit-log.read Distinct EntityNames for filter dropdown
GET /export audit-log.read `format=xlsx

Response shapes — quick reference

// Common envelopes
PagedResult<T>            = { items: T[], totalCount: number, page: number, pageSize: number }
LoginResultDto            = { accessToken: string, refreshToken: string, expiresAt: string,
                              tokenType: 'Bearer', user: CurrentUserDto }
TokenResponse             = { accessToken: string, refreshToken: string, expiresAt: string }
CurrentUserDto            = { id, code, email, userName, fullNamePrimary, fullNameSecondary,
                              userType, preferredLanguage, preferredTheme, avatarUrl, … }
ProblemDetails            = { status, title, detail?, errors?, errorCode?, required?, ... }
DocumentLinkResultDto     = { id, documentId, ownerType, ownerId, linkedAt }

For the full set of DTO record signatures, search src/AssetTracking.Application/DTOs/ — the records mirror the JSON fields one-to-one (PascalCase in C#, camelCase on the wire via JsonNamingPolicy.CamelCase).

Where to go next

To learn… See
Module business rules + handler signatures 02-backend-modules
Column types, indexes, FKs 03-database
Frontend service that calls each endpoint 04 §Service layer
Build/deploy/seed 06-operations