Skip to article

HTTP API reference

Compatible native/API origin: https://forge.readyit.dk. The browser product uses https://opalspan.com; the old host remains for installed clients. Routes below are relative to the compatible origin.

Authentication, selection and JSON

Human application requests use a valid bearer access token for the service and the X-Forge-Workspace-Id header selecting an authorized workspace UUID. Use the supported login flow; do not extract credentials from a profile or paste tokens into documentation, logs or customer messages.

GET /api/v1/tenants discovers the authenticated identity's memberships before selection. The header selects among existing memberships; it grants no authority. Human Admins and Technicians have ordinary workspace-wide access under the current workspace-humans-v2 policy. Settings, accounts, roles and invitations remain Admin-only.

Machines retain explicit test scope and are denied human directory, case, overview and generic history APIs. A human role claim does not turn a machine principal into a human.

Send request objects as application/json. Wire fields are snake_case; UUIDs are strings and timestamps use ISO 8601 with an offset. Nullable/optional response fields may be omitted. DTO names below identify documented wire contracts, not URLs or assembly-download instructions.

HTTP responses contain their actual object, array or page—not the CLI ok/data/next_actions envelope. Treat customer/history responses as private and honor Cache-Control: no-store. After a mutation times out, its completion may be unknown; do not assume it is safe to replay.

Connected work history and explicit diagnostic plans

Method Route Contract
GET /api/v1/activity-history WorkbenchActivityHistoryPage; bounded q/actor/device/caseId/company/contact, since/until/filter/scope/kind/cursor/limit; exact query names follow WorkbenchHistoryApi.
GET /api/v1/activity-history/{conversation}/{run} WorkbenchHistoryRun; exact frozen context/observed lifecycle, private handback only under conversation authority, and bounded operation/access children.
GET /api/v1/case-history/{id} WorkbenchCaseTimelinePage; prospective Case/draft chronology, kind/period/cursor/limit filters and explicit historical coverage.
GET /api/v1/diagnostic-plans/devices/{id} DeviceDiagnosticCapability; actual platform/helper availability, sections and limitations. No collection.
POST /api/v1/diagnostic-plans/prepare DeviceDiagnosticSelection → private exact DeviceDiagnosticPlan. No access acquisition or collection.
POST /api/v1/diagnostic-plans/start StartDeviceDiagnosticPlan → exact DeviceDiagnosticRun; explicit collection under current native authority.
POST /api/v1/automations/{id}/runs/{run}/continue-recorded Original run owner and frozen configuration/targets; UUID request_id and original configuration_version as expected_version. Existing /continue remains current-definition based.

History has stable typed source identities. Access-period context is never promoted into an individual run result. Frozen uncertain_effects and uncertain terminal state remain visible; old unrecorded facts are not reconstructed. Grouped usage labels can contain private accepted intent; keep complete reports private, not in operational logs.

Diagnostic start binds a private review token to actor/workspace, target/Case revision, signed helper and expiry. Collection invokes no model and changes no Monitoring configuration. The exact UUID reconciles a lost acknowledgement; never automatically replay an uncertain collection. Current device-user consent, work allowances and agreement/monetary checks remain authoritative. Optional interpretation is a separate explicit request.

The additive startup source contract uses GET /api/v2/fleet/devices/{uuid}/startup-evidence → Forge.Contracts.V2.DeviceStartupEvidenceRead under the same human device permissions. It returns original native duration/period/provenance, latest collection coverage and an explicitly comparable prior baseline, never uptime or a telemetry-derived guess. Canonical refresh requires the existing plan with sections: ["startup"]; existing profiles exclude it. Old signed helpers remain unsupported until compatible helper publication. See explicit startup source semantics; source availability is not a claim that new helpers or Bridge are deployed.

Automation and telemetry

Method Route Purpose
GET /api/v1/automations, /{id}, /limits, /templates, /models Bounded configuration, current offer capabilities and model/template choices. ID suffixes are under /api/v1/automations.
POST /api/v1/automations/quote, /confirmations AutomationConfiguration target aggregate or AutomationPrepareWrite exact private human review; neither approves or reserves work.
GET /api/v1/automations/confirmations/{id} Original actor's exact AutomationHumanReview and decision state.
POST / PUT /api/v1/automations, /{id} Apply exact AutomationWriteRequest with consumed human confirmation; changed payload/revision conflicts.
POST /api/v1/automations/{id}/pause, /resume, /adopt, /delete AutomationActionRequest; resume/adopt prepare at /{id}/confirmations/{operation}.
GET /api/v1/automations/{id}/runs, /runs/{run} Persistent-conversation wake snapshots, bounded results and distinct AI/new/reused work accounting.
POST /api/v1/automations/{id}/runs/{run}/answer, /cancel, /continue Answer exact waiting decision, cancel exact run, or start explicit new run without uncertain-effect replay.
GET /api/v1/automations/{id}/runs/{run}/notifications Private metadata-only outbox/delivery state.
GET / POST /api/v1/automations/notification-settings Current member's critical-service email preference/revision.
POST /api/v1/automations/critical-email/{conversation}/{run}/prepare, /send Exact AutomationCriticalEmailRequest review, then AutomationCriticalEmailSend; verified workspace members only, not a mailbox-delivery assertion.
GET /api/v1/device-telemetry/signals, /activation, /instruction Allowed signals and exact configured notice/instruction applicability, not consent.
GET /api/v1/device-telemetry/devices/{id}, /history DeviceTelemetryView[] or bounded DeviceTelemetryHistory; stored metadata, no fresh diagnostic work.
POST /api/v1/device-telemetry/devices/prepare, /devices DeviceTelemetryChange; exact single/bulk revisions and private human confirmation.
GET / POST /api/v1/device-telemetry/new-enrollment-default Admin-only future-enrollment default; prepare at /prepare, never changes current devices.
POST /api/v1/device-telemetry/instruction/prepare, /instruction, /consent/prepare, /consent TelemetryProcessingInstruction or DeviceTelemetryConsent, attributable exact holder/device-user review; missing consent/necessity remains off.

The /browser/v1/automations/... adapter requires the genuine cookie-authenticated human and CSRF header for unsafe requests. Only that protected human path or the existing exact Mech controller may answer a configuration review. A bearer client can prepare/read/apply a receipt but cannot manufacture human approval. Existing /api/v1/mech/conversations/{id}/decisions remains the separate pending-runtime decision read/answer/status adapter; it does not create a new confirmation engine or grant a role.

AutomationRunView identifies its configuration revision, current owner, actual targets, trigger, status/reason and accounting. work_session_budget_minor is a zero-default whole-run DKK monetary authority, not AI credits or usage. Native accepted-operation recording owns actual sessions; configuration/claim creates no charge. A telemetry receipt acknowledges metadata only; it is not verified repair, consent, diagnostic completion or a credit/work-session charge. Rescue is excluded. Current catalogue discovery includes telemetry-light-v1 and telemetry-full-v1, with cumulative native capability and independent privacy/local consent gates. Source support is not a claim of deployed activation.

Workspace billing

Method Route suffix Purpose
GET /status, /work-sessions, /budget Retained subscription/capacity, actual and provisional work, accepted/in-flight monetary exposure.
POST /compare, /session-quotes, /budget-quotes Full-month applicable-plan comparison, actual target-set exposure, or exact budget proposal; never approval.
GET /confirmations/{id} Original Admin's retained quote and human-confirmed state.
POST /hosted-subscriptions, /device-slots, /native-top-ups Explicit request-bound hosted purchase; returns private provider handoff, not payment evidence.
GET /hosted-subscriptions/{id}, /device-slots/{id}, /native-top-ups/{id} Original retained/provider-confirmed purchase outcome.
POST /customer-portal, /plan-change-portal Stripe invoice/billing/cancellation or validated same-generation target-plan handoff.

plan-change-portal accepts {} for the retained two-plan route or {"target_plan_id":"fleet"} for an explicitly configured target. Caller-selected prices, provider accounts, subscriptions and return URLs are refused. The bearer boundary has no budget-confirmation endpoint. The exact authorized human confirms through the protected browser or Mech controller, where workspace/run/quote/expiry and current authority are rechecked atomically. Provider Checkout/card/SCA/consent and Auth0 signup/sign-in remain browser flows. No machine principal gains billing administration. Human-operated CLI is not required.

Public downloads

These GET routes require no login:

Route Result
/api/v1/client-configuration ClientConfigurationDto: public bridge_url, auth0_issuer, auth0_client_id, auth0_audience, expected_publisher. No user credentials or memberships.
/api/v1/cli/releases/latest?platform=win-x64 CliReleaseDto: schema_version, version, platform, expected_publisher, installer, archive. Each artifact contains url, size, sha256.
/api/v2/cli/releases/latest?platform=linux-x64 CliReleaseV2Dto: numeric schema_version, version, platform, signed package, target_payloads.
/api/v2/cli/releases/linux-x64/{version} Metadata for one verified Linux OpalSpan Agent CLI release under its retained signed release identity.
/download/cli/win-x64/latest/installer HTTP 200 verified file (503 when unavailable) to the selected Windows setup.
/download/cli/win-x64/latest/archive HTTP 200 selected verified Windows ZIP; 503 when unavailable.
/download/cli/linux-x64/latest/installer HTTP 200 verified file (503 when unavailable) to the selected Linux setup.
/download/cli/linux-x64/latest/archive HTTP 200 selected verified Linux archive; 503 when unavailable.
/download/cli/linux-x64/latest/manifest Redirect to version-specific Linux release metadata.

PortableArtifactEnvelopeDto contains schema_version, algorithm, key_id, manifest and signature. The manifest/signature encode exact signed artifact identity, not a new trust key. Clients must verify with the approved independently pinned key, not trust arbitrary keys supplied beside a download. Use supported setup rather than implementing partial signature validation.

Installer/archive aliases select capable Windows/Linux releases, not necessarily legacy latest catalogue entries. When downloading several artifacts, resolve a single release and use its immutable locations; independent latest requests can straddle a release change. Unavailable downloads fail explicitly.

Identity and enrolled devices

Method Route Response
GET /api/v1/tenants Array of TenantMembershipResponse.
GET /api/v1/me TechnicianIdentityResponse for the selected workspace.
GET /api/v1/devices Optional exact company UUID; array of authorized DeviceSummaryResponse, filtered in the same workspace query.
GET /api/v1/devices/{id} Authorized DeviceDetailsResponse.
PUT /api/v1/devices/{id}/metadata Updated device details; input UpdateDeviceMetadataRequest.

Key fields:

  • TenantMembershipResponse: tenant_id, name, role, current, optional legacy auth0_organization_id. Use the OpalSpan workspace ID for selection.
  • TechnicianIdentityResponse: id, subject, display_name, optional email, tenant_id, tenant_name, role, device_access_mode.
  • Device summaries: id, machine_name, device_identifier, presence, last_seen_at, host_version, is_revoked, platform, optional descriptive labels and connection/transport metadata.
  • Device details add revocation, current lease/technician and SSH identity information. Compatibility fields do not imply a supported legacy transport; the released path is native relay.
  • UpdateDeviceMetadataRequest: nullable display_name, primary_user_name, organization_name, and observed version. These do not update company/contact relationships.

Use opalspan device connect, exec, upload and download for remote work. The CLI owns key handling, exclusive leases, grant renewal and relay admission. There is no generic HTTP “execute a command” API in this reference.

Device administration and enrollment housekeeping (S4)

Method Route Contract
PUT /api/v1/devices/{id}/work-hours SetDeviceWorkHours: schedule, expected_version → advisory DeviceWorkGuidance. DeviceEditor.
PUT /api/v1/devices/{id}/capacity SetCapacityActiveRequest: active, version. Admin only.
POST /api/v1/devices/{id}/revoke RevokeDeviceRequest: reason, version; closes live control, retains history. DeviceManager.
GET /api/v1/device-transfers/{id} Metadata-only DeviceTransferResponse with version; device ID is independently authorized. DeviceManager.
GET /api/v1/enrollment/options EnrollmentOptionsResponse: platforms, share_issuance_enabled; verified current release choices. DeviceManager.
GET /api/v1/enrollment EnrollmentTokenResponse[], lifecycle and version; never codes or recoverable URLs. DeviceManager.
GET /api/v1/enrollment/{id} Exact authorized EnrollmentTokenResponse; transferred device IDs are suppressed. DeviceManager.
POST /api/v1/enrollment/{id}/revoke EnrollmentVersionRequest: version; unused token only, not a completed device. DeviceManager.
POST /api/v1/enrollment-shares/{id}/revoke EnrollmentVersionRequest: version → metadata-only EnrollmentShareResponse. DeviceManager.
POST /api/v1/enrollment-shares/{id}/extend ExtendEnrollmentShareRequest: expected_expires_at, additional_minutes, version. Admin only.

Device details, enrollment and share status add opaque version fields. New agent clients preserve the observed revision; compatible legacy optional fields keep their meanings. Revisions exclude heartbeats and lease renewal. Work hours use their independent work_guidance.version, including none; they remain advisory, not an execution policy or scheduling promise. Stale versions return 409 stale_version. Share extension also compares the previous exact expiry, never revives a used/revoked/run-owned link or rotates its URL/code/pinned release. GETs return no-store metadata. No client automatically retries an uncertain mutation.

Mech uses the same service transaction and shared S3 confirmation engine for consequential changes, binding actor/workspace, exact target/version/payload, conversation/run/scope and expiry. There is no public DTO approval field or borrowed CLI device lease. Workspace Rescue history remains GET /api/v1/temporary-devices[/{id}]; read visibility grants no owner-only control/cleanup credential. Enrollment receipts and reconnects are not an additional charged work session. Transfer status never returns acceptance or candidate credentials, and prepared does not mean completed. Windows/Linux share all server paths; macOS remains a technical preview with unchanged native trust.

Team, invitations and workspace settings

Method Route Contract
GET /api/v1/team TeamMemberResponse[], including opaque version, effective access, capacity and the existing AI limit.
GET /api/v1/team/{id}/device-access DeviceAccessResponse with the same member version.
GET /api/v1/team/{id}/device-access/assignments?limit=25&cursor=...&version=... MemberAssignmentPage: stored/effective modes, current policy/edit restriction, assignment-specific version, exact page/total and final completeness proof.
POST /api/v1/team/{id}/device-access/review ReviewMemberAssignmentsRequest: exact version/modes and additions/removals; complete-set mode replacement additionally requires verified traversal proof and ≤1,000 IDs.
POST /api/v1/team/{id}/device-access/apply Original request_id and actor/workspace/member-bound review_token; exact idempotent application or status-only replay with current authority.
PUT /api/v1/team/{id}/role SetRoleRequest: role, version.
PUT /api/v1/team/{id}/spend-limit SetMemberSpendLimitRequest: monthly_credits, version; null removes the member-specific limit.
PUT /api/v1/team/{id}/capacity SetCapacityActiveRequest: active, version.
PUT /api/v1/team/{id}/device-access SetDeviceAccessRequest: access_mode, device_ids, version; v2 human scope remains policy-managed.
DELETE /api/v1/team/{id}?version={version} Removes the exact current member; the last active Admin is protected.
GET /api/v1/team/invitations Metadata-only TeamInvitationResponse[], lifecycle and versions.
POST /api/v1/team/invitations/issue IssueTeamInvitationRequest: stable request_id, email, device_access_mode, device_ids.
POST /api/v1/team/invitations/{id}/issue Same issuance contract with current version; recipient cannot change or device access expand.
POST /api/v1/team/invitations/{id}/revoke TeamVersionRequest: current version.
GET /api/v1/settings WorkspaceSettingsResponse: policy, retention, limit availability and version.
PUT /api/v1/settings/retention SetAuditRetentionRequest: audit_retention_days, expected_audit_retention_days, current version.

Issuance returns TeamInvitationDeliveryResponse: state, invitation metadata, customer_origin advertised by the authenticated Bridge, and a one-time link only for new delivery. The browser/customer origin can differ from a retained native API origin. Keep the link in the required protected CLI output, never stdout, model context or logs. Identical request replay returns status_only with no link; changed payloads fail. Replacement/revoke invalidate old links and pending acceptance handles. Mech uses server-rendered exact-change confirmation and a human-only private download; its model cannot approve or read that link. Target versions, actor/run/scope, expiry and the exact payload are rechecked in the locked service transaction. Existing legacy request fields remain compatible; new agent clients always send the current version. No mutation is automatically retried after an ambiguous result.

Mech controller decisions

The required bearer decision adapter shares Mech's existing controller transition; its publication is recorded on the release page, not inferred from this reference. It does not create a conversation/run, let a model approve itself or grant a higher role. Only the signed-in current human controller may answer; the underlying operation retains its own live workspace/device/role and exact-change checks.

Method Route Contract
GET /api/v1/mech/conversations/{id}/decisions MechDecisionResponse: current pending server-rendered question/proposal or no_pending_decision.
GET /api/v1/mech/conversations/{id}/decisions/{decision} The exact pending decision or durable approved/declined/consumed metadata; unavailable decisions are not inferred successful.
POST /api/v1/mech/conversations/{id}/decisions/{decision} MechDecisionAnswerRequest: observed run_id, conversation version, explicit approved, exact payload_sha256; ordinary questions instead supply answer with null approval/digest.

A confirmation requires an empty answer and matching run, version and digest. Edits, expiry, changed controller or lost authority refuse it. Approval is recorded once and consumed in the actual service transaction; it is not completed-execution proof. No private invitation URL, server-only proof or execution credential is returned. Responses are no-store; API requests use bearer authentication and do not borrow the web cookie. After an ambiguous submission, read the same decision and target state before retrying, rather than generating another approval.

Enrollment sharing

For authorized human device managers:

Method Route Request → response
POST /api/v1/enrollment-shares CreateEnrollmentShareRequest → CreatedEnrollmentShareResponse.
GET /api/v1/enrollment-shares/{id} EnrollmentShareResponse.
DELETE /api/v1/enrollment-shares/{id} Updated EnrollmentShareResponse.

Creation input is label, platform win-x64/linux-x64 or Mac-preview osx-arm64/osx-x64, and optional expires_at. Null expiry means 24 hours; the maximum is seven days. Legacy unkeyed creation returns share and one-time capability url. Current agent issuance uses POST /api/v1/enrollment-shares/issue with retained idempotency_key and EnrollmentShareDeliveryResponse. Status-only replay need not return a URL; never fall back to legacy create after uncertainty. Optional support_access_mode/classification and exact replacement/intake/association identity retain separate authority. Never store that URL in an ordinary request log or operational receipt.

Status includes id, label/platform/release metadata, created_at, expires_at, state, and optional downloaded_at, consumed_at, online_at and enrolled_device_id. Status does not return a recoverable capability or code. Consumption and authenticated online presence are distinct.

Use enrollment guidance for delivery and installation; native code redemption and package trust belong to the signed client, not a browser script.

History and the work map

GET route Query and response
/api/v1/access-sessions Optional actor, device, state, since, until, cursor, limit; returns AccessSessionPage. State is active or ended.
/api/v1/access-sessions/{id} Optional cursor, limit; returns AccessSessionDetail.
/api/v1/access-activity/uncorrelated Optional device, cursor, limit; returns UncorrelatedActivityPage.
/api/v1/temporary-devices Optional history, cursor, limit; returns TemporaryDevicePage. history=true selects ended/promoted history.
/api/v1/temporary-devices/{id} TemporaryDeviceDetail, with separately authorized promotion information.
/api/v1/overview Optional customer, case, technician, activity, cursor; returns OverviewSnapshot. Activity is all, active or idle.

Session pages use items and next_cursor. Session detail instead has session, events, next_cursor. Session listing defaults to 25 items and detail to 50; both accept limits 1–100. since is inclusive and until exclusive on the access period's start.

AccessSessionSummary contains id, actor_id, actor_name, target_kind, target_id, target_name, started_at, last_observed_at, optional ended_at, state, end_reason, client_version, host_version. An AuditEventResponse includes event/actor/target identifiers, outcome, timestamps, optional version/transfer-hash metadata and provenance. These are metadata, not commands or file contents.

TemporaryDeviceDetail contains temporary, optional access_session_id, optional promotion, cleanup_outcome, optional cleanup_observed_at, and cleanup_provenance. A promotion's health_confirmed_at comes from the authenticated permanent device; a cleanup observation does not certify remote process removal. History access does not confer Rescue control.

OverviewSnapshot contains workspace_id, observed_at, policy_version, customer_context_available, enrolled/online counts, nodes, edges, groups, omitted_connections, next_cursor. Nodes identify company, case, technician, device, session or Rescue records. Groups include authorized total and expanded counts. The bounded map describes explicit relationships and observed access, not agent jobs or repair progress.

Customer usage and Mech lifecycle

Current summary is v3; v1/v2 projections remain old-client compatibility. Mech settings include Plan; exact confirmed changes snapshot subsequent runs and cannot modify an active run or grant authority.

Method Route Contract
GET /api/v1/usage/ai Required UTC since/until (at most 31 days); optional conversation/run/controller/before, caseId/company/contact, groupBy/groupCursor/groupLimit, and independent Admin-only workSession; customer retail report.
GET /api/v1/work-sessions Admin only; since, until, technician, cursor, limit; WorkSessionReportPage.
GET /api/v1/work-sessions/current/v3 Admin-only frozen current-month retail estimate, same as Billing.
GET /api/v1/work-sessions/{id}/events Admin only; chronological content-free events, cursor, limit.
GET /api/v1/releases Signed global Host release metadata, never publication rights.
GET /api/v1/mech/capabilities Permitted model/reasoning/scope selections, customer retail rates and attachment limits.
GET / POST /api/v1/mech/conversations Paged read (archived, cursor, limit); create with stable request_id, optional title/model/reasoning, scope_mode, device_ids, case_id.
GET /api/v1/mech/conversations/{id} Private MechConversationDetail: metadata, transcript and activity; proposals are read via the shared decision API.
POST .../{id}/rename Current version, exact title.
POST .../{id}/archive, /claim, /release Current version; idle and controller checks. Claim cannot take another controller.
POST .../{id}/settings Current version, allowed model, reasoning.
POST .../{id}/scope Current version, scope_mode, device_ids; never wider than a Mech caller's existing fence.
POST .../{id}/context Current version, device_ids, nullable case_id; focus is not authority.
POST .../{id}/messages Current version, stable client_message_id, text, optional owned attachment_ids; returns MechRunStart without holding an observer stream.
POST .../{id}/continue Current version, exact prior run_id, stable client_message_id; explicit interrupted-work recovery.
POST .../{id}/stop, /cancel Current version, exact run_id; fences only that run.
POST .../{id}/guidance Exact run_id, stable client_message_id, bounded text; lost injection acknowledgement stays uncertain.
GET .../{id}/runs, /runs/{run} Paged run identities/status; exact-run private transcript and operation results.
GET .../{id}/events after_sequence, limit; bounded replay page. Private decision events are redacted; use the shared role-checked decision API.
GET .../{id}/files, /files/{file}/download Bounded file metadata page (offset, limit) or integrity-checked private bytes; opaque IDs, no server paths.
POST .../{id}/attachments Bounded multipart request_id and customer files, same classifier and private store as browser Send; stable staging before explicit message submission.

... means /api/v1/mech/conversations. Page limits are 1–100. Create and message keys are canonical UUIDs. created:false means no new run or provider call; the response run_id identifies the original input run even if conversation metadata now identifies a newer run. Every mutation rechecks the current controller, role, scope and revision under the workspace lock; no client adopts another client's lease. Read status and results after a timeout—never automatically replay a mutation. Private bodies belong in protected local files, not stdout, Git or operational logs. Pending questions/meaningful decisions use the existing controller decision routes; a boolean or model statement cannot grant additional authority.

Customer directory

In the table, {collection} is companies or contacts.

Method Route Request → response
GET /api/v1/{collection} search, archive, cursor, limit; contacts also accept company. Returns CompanyPage or ContactPage.
GET /api/v1/{collection}/{id} CompanyDto or ContactDto, including archived records.
POST /api/v1/{collection} CompanyWriteRequest or ContactWriteRequest → created DTO, HTTP 201.
PUT /api/v1/{collection}/{id} Full write request with current version → updated DTO.
POST /api/v1/{collection}/{id}/archive DirectoryVersionRequest → updated DTO.
POST /api/v1/{collection}/{id}/restore DirectoryVersionRequest → updated DTO.
GET /api/v1/devices/{id}/customer-links DeviceCustomerLinksDto.
PUT /api/v1/devices/{id}/customer-links DeviceCustomerLinksRequest → updated links.

Creation also accepts optional persisted idempotency_key. Write fields are display_name (1–200 characters), optional email (one address, up to 320), optional phone (up to 64), and version for updates. Contacts add nullable company_id. Companies add nullable primary_contact_id: omitted retains the recorded designation, explicit null clears, and a UUID selects an active same-workspace contact already linked to that company. An unchanged archived contact may be retained, not newly selected. Creation accepts only omitted/null designation; link a contact after creating the company, then update at its current version. Clear/change a company's designation before moving or unlinking that contact. Selection never assigns contact membership or access. Other omitted optional content fields still clear them during an update. DirectoryVersionRequest is { "version": 1 }, with the actual current version substituted.

Company/contact DTOs include the content plus id, archived_at, version, created_at, updated_at. Contacts also return a display-only company reference. A reference has id, display_name, archived_at. Companies return primary_contact_id and primary_contact, both null when unset. A recorded primary contact includes id, available, nullable display_name, email, phone, archived_at. Availability requires the contact still belongs to this company in the same workspace; archive is reported separately. A missing/foreign/mismatched relation retains only the recorded ID with available: false and null customer facts. No list ordering creates designation. Directory responses advertise X-Forge-Company-Primary-Contact-Version: 1; establish this capability before relying on a write, as older servers may ignore an additive field. The native CLI refuses unsupported designation writes and reconciles uncertain receipts rather than replaying them.

Device-link input must explicitly include both nullable company_id and primary_contact_id, plus its independent version. Null unlinks. The response includes device_id, the IDs, display-only company and primary_contact references, and the link version.

Pages have items, next_cursor; limit is 1–100, default 25. Archive is active (default), archived or all. Search is a bounded case-insensitive literal match over name, email, phone and authorized related records. Preserve the same filters when continuing a cursor.

See customers for full examples, preserved archived references and keyed-versus-unkeyed create/retry behavior.

Cases and narrative

All routes below start with /api/v1/cases. {id} is the case UUID.

Method Suffix Request → response
GET (none) Optional search, status, company, contact, archive, one of device/session, cursor, limit → CasePage<CaseDto>.
POST (none) CaseCreateRequest → CaseDto, HTTP 201.
GET /members cursor, limit → CasePage<CaseAuthorDto>; existing human collaborators, not account management.
GET / PUT /{id} Read CaseDto / replace with CaseWriteRequest.
POST /{id}/status CaseStatusRequest → CaseDto.
POST /{id}/close, /reopen, /archive, /restore CaseVersionRequest → CaseDto.
GET / POST /{id}/collaborators Array of CaseAssigneeDto / CaseAssigneeRequest → CaseDto.
DELETE /{id}/collaborators/{member} Body CaseVersionRequest → CaseDto.
GET / POST /{id}/links Array of CaseLinkDto / CaseLinkRequest → CaseDto.
DELETE /{id}/links/{link} Body CaseVersionRequest → CaseDto.
GET / POST /{id}/notes Paged CaseNoteDto / CaseNoteCreateRequest → CaseNoteDto, HTTP 201.
GET / PUT /{id}/notes/{note} CaseNoteDto / CaseNoteWriteRequest → updated note.
GET /{id}/notes/{note}/revisions Paged CaseNoteRevisionDto.
GET / POST /{id}/drafts Paged CaseDraftDto / CaseDraftCreateRequest → CaseDraftDto, HTTP 201.
GET / PUT /{id}/drafts/{draft} CaseDraftDto / CaseDraftWriteRequest → updated draft.

Every lifecycle suffix above is relative to /{id}; for example, reopening uses /api/v1/cases/{id}/reopen. Paged requests accept cursor and limit (case discovery 1–100; member/note/draft/revision 1–50; default 25), returning items and next_cursor. Requests are limited to 128 KiB and reject unknown author/review claims.

Request shapes

DTO Fields
CaseCreateRequest Required title; optional idempotency_key, description, company_id, primary_contact_id, device_id, access_session_id.
CaseWriteRequest Required version, title; replacement description, company_id, primary_contact_id.
CaseVersionRequest Current version.
CaseStatusRequest Current version, status: open, in_progress or waiting.
CaseAssigneeRequest Current case version, user_id.
CaseLinkRequest Current case version, kind: device or session, and target_id.
CaseNoteCreateRequest Required idempotency_key, body; kind defaults to finding, basis to observation; optional source, agent_name, references.
CaseNoteWriteRequest Current note version, replacement note fields; no idempotency key.
CaseDraftCreateRequest recipient_address, subject, body; optional idempotency_key, recipient_contact_id.
CaseDraftWriteRequest Current draft version plus replacement draft fields. Every update clears review.

Case titles allow 200 characters and descriptions 8,000. Note/draft bodies allow 16,000; draft subjects 300 and recipient addresses 320. Notes accept kinds finding, action, result, next_step and bases observation, inference. Each of at most ten references has kind (device, session, authorized audit) and target_id.

Response and review contracts

  • CaseDto includes metadata, customer references, creator, timestamps, status, closed_at, archived_at, version.
  • CaseAuthorDto contains id, display_name, former_member.
  • CaseLinkDto contains its own unlinkable id, evidence, created_at. Unavailable evidence does not expose a target ID or label.
  • CaseNoteDto contains id, creator, created_at, current. current is an immutable attributed revision containing version, content, basis/source/agent labels, author, recorded_at, references.
  • CaseDraftDto contains recipient/contact, subject/body, its own version, author/timestamps, state, reviewed_version, reviewer, reviewed_at. It is a draft, never a delivery receipt.

Mech review_draft renders the exact recipient/subject/body/revision and waits for genuine current-controller approval, consuming its bound decision in the same locked case service transaction. Review uses current CasesManage, not Admin-only team permission. The existing website review remains cookie/ antiforgery-protected. Case drafts have no send API; customer messages below are a separate, separately gated capability. These additions require compatible Bridge/CLI publication; source alone is not a released-capability claim. See the case workflow.

Work item: opalspan#1019. Workspace-to-customer case updates (started, update, finished) with exact preview, human approval and one-shot dispatch. Real delivery is separately gated and off by default; availability.can_prepare and availability.can_deliver report independently with stable reasons. A gated-off send is a known refusal (503 customer_mail_unavailable) before dispatch: the message stays prepared and its link stays unpublished. Contracts: CustomerCommunicationContracts.cs.

Method Route Purpose
GET / PUT /api/v1/customer-communications/settings Human case readers read brand, verified reply-to, actual platform sender and availability; PUT is workspace Admin only.
GET /api/v1/customer-communications/instruction Human case readers see the selected publication and applicable attributed workspace instruction; publication is unselected by default.
POST /api/v1/customer-communications/instruction/reviews Admin freezes a protected review of the exact underlying agreement, supplemental documents, settings revision and SES provider/region/retention binding. Its digest identifies content, never acceptance.
GET /browser/v1/customer-communications/instruction/reviews/{id} Same signed-in human Admin reads the original protected review after navigation or reload; no creation, acceptance or state mutation.
POST /browser/v1/customer-communications/instruction/reviews/{id}/decision Cookie-authenticated same human Admin accepts or declines the review; antiforgery and fresh actor/version/binding checks. No bearer/CLI decision route.
GET /api/v1/customer-communications/settings/reply-to-candidates Admin-only members with current verified sign-in address evidence.
GET / POST /api/v1/customer-communications/suppressions, /{id}/lift Admin view; only human-reported returned suppressions can be lifted, never a recipient opt-out.
GET / POST /api/v1/cases/{id}/customer-messages, /prepare Bounded list; freeze one exact message for an explicit case_customer, device_owner or selected_responsible_contact recipient. Nothing is sent or published.
GET /api/v1/cases/{id}/customer-messages/{message} Message, frozen HTML with capability fragments redacted, digests, customer-page projection and delivery state.
POST /api/v1/cases/{id}/customer-messages/{message}/approvals, /approvals/{approval} (GET) Request and read a one-use approval for the same signed-in human; returns human_review_url. A digest is content identity, not consent.
POST /api/v1/cases/{id}/customer-messages/{message}/send Consume that human's website-approved approval in admission, then dispatch once. Repeats return the stored receipt.
POST /api/v1/cases/{id}/customer-messages/{message}/cancel, /returned Cancel a prepared message; report human-observed returned mail (workspace-scoped suppression).
GET / POST /api/v1/cases/{id}/customer-links, /{link}/revoke Link state and revocation. Links exist only through a message.
POST /customer-case/v1/view, /preferences/status, /preferences/stop Anonymous, no-store/noindex/no-referrer, same-origin JSON with the fragment token in the body. Invalid, expired and revoked links share 404 link_unavailable; preferences are opt-out only for one case and never return case content.

The /browser/v1/... adapter adds cookie-only /approvals/{approval}/decision and /confirm; only there does a prepared preview contain live capability fragments. Provider acceptance is never inbox delivery, and bounces are not observed automatically. Generic provider, configuration or rate-limit failures never suppress a recipient.

Customer mail uses the maintained SESv2 SDK in eu-north-1, with automatic request retries and SDK logging disabled. The fixed platform From, identity ARN and named configuration set are frozen with the exact email; ordinary replies use the workspace member's verified Reply-To. No contact-list or tracking API is used. The customer transport is separate from Microsoft member/billing mail and Auth0 sending. Its dedicated protected credential file and explicit profile never use the operator/default credential chain. Every delivery, runtime and publication switch defaults off; sender, identity, credentials, configuration-set and document bindings default empty. A verified domain alone does not enable customer sending. Private preparation remains available while delivery is unavailable. workspace_instruction_missing means no current workspace instruction matches the reviewed settings, actor and published documents. The instruction's acceptance records UTC time, identity/version, request, underlying agreement, three supplement IDs/hashes, provider, region and retention binding; it never supplies approval for an individual send. SES acceptance includes its provider message ID but proves neither inbox arrival nor observed recipient feedback.

Errors and retries

Common status meanings:

HTTP status Action
400 Correct malformed input, unsupported selection or invalid relationship.
401 / 403 Reauthenticate or verify current membership/feature authority; do not retry under an unrelated identity.
404 The record is unavailable under current scope; it may be missing or inaccessible.
409 Reload the current version or resolve lifecycle/idempotency conflict.
413 Reduce the bounded request body.
429 Respect throttling; slow down rather than repeating immediately.
503 Service or verified release unavailable; inspect state before retrying a write.

Many application failures return { "error": "<code>", "message": "<detail>" }. Overview has a separate code error field; authorization/routing failures may not return an application JSON body. Do not assume one universal error envelope.

Important feature codes include directory_not_found, directory_conflict, invalid_case, case_conflict, case_archived and case_storage_unavailable. Note creation requires its original idempotency key. Company/contact/case/draft creates support capability-gated optional persisted keys; unkeyed creates remain non-idempotent. Replay preserves actor/workspace/parent and unchanged payload/current content. Never remove a key or change payload after an uncertain keyed mutation.

Device-work accounting

GET /api/v1/device-work/{operation-id} reads the current actor's content-free native lifecycle and distinct commercial references. It requires current selected workspace membership; another user's identifier grants no access. It returns operation_id, state, new_session_ids, reused_session_ids, platform_covered_session_ids, new_sessions and reused_sessions. Reservations are not usage. Missing or uncertain evidence never means continued execution or completed repair and is not permission to replay a command.

Only the authenticated native Host may report device acceptance, continuity and completion. A signed-in agent cannot manufacture usage by submitting command text, filenames, traffic, an inference decision or an untrusted lifecycle assertion. Mech's server-owned run references share the same unit rather than create charges.