An official website of the United States GovernmentUnclassified
Seal of the Department of the NavyDepartment of the NavyProject ROUNDTABLE Docs
API reference

Government API

Government ledger, publishing, and administration route handlers

Every route in this page starts with a government session check. A government session is not enough for every mutation: readonly users, POCs, command leads, administrators, and system administrators have different gates. Examples use admin@navy.mil, sysadmin@navy.mil, NAVSEA, NIWC PAC, and Pelagic Dynamics.

Admin commands

GET /api/gov/admin/commands

Auth: admin-tier government session (GOV_COMMAND_LEAD, GOV_ADMIN, or GOV_SYSTEM_ADMIN). Success: 200.

curl http://localhost:3000/api/gov/admin/commands -H 'cookie: next-auth.session-token=SESSION'

Example: { "data": [{ "id": "clx-navsea", "name": "Naval Sea Systems Command", "abbreviation": "NAVSEA", "echelon": "SYSCOM", "parentId": null, "location": "Washington, DC", "mission": "..." }] }.

Errors are 401, 403 where the route helper denies the role, and 500 for database failure.

POST /api/gov/admin/commands

Auth: GOV_SYSTEM_ADMIN; success: 201.

FieldTypeRequired?Constraints
namestringYesTrimmed, 1–200.
abbreviationstringYesTrimmed, 1–50.
echelonstringYesTrimmed, 1–100.
parentIdstring/nullNo1–255 or null.
locationstring/nullNoMax 200; nullish.
missionstring/nullNoMax 4,000; nullish.
curl -X POST http://localhost:3000/api/gov/admin/commands \
  -H 'content-type: application/json' -H 'cookie: next-auth.session-token=SYSTEM_SESSION' \
  -d '{"name":"Demo Maritime Integration Office","abbreviation":"DEMO MIO","echelon":"Office","parentId":"clx-navsea","location":"San Diego, CA","mission":"Coordinate pilot demonstrations."}'

Success: { "data": { "id": "clx-command", "name": "...", "abbreviation": "DEMO MIO" } }. Errors: 400 invalid body, 401/403 gate, 409 duplicate name/abbreviation, 500.

GET|PATCH|DELETE /api/gov/admin/commands/[id]

Auth: GET uses government visibility; PATCH/DELETE require system-admin authority.

PATCH fields:

FieldTypeRequired?Constraints
namestringNoTrimmed, 1–200.
abbreviationstringNoTrimmed, 1–50.
echelonstringNoTrimmed, 1–100.
locationstring/nullNoMax 200.
missionstring/nullNoMax 4,000.
curl -X PATCH http://localhost:3000/api/gov/admin/commands/clx-demo \
  -H 'content-type: application/json' -H 'cookie: next-auth.session-token=SYSTEM_SESSION' \
  -d '{"mission":"Updated pilot coordination mission"}'

GET returns { "data": <command with relations> }; PATCH returns { "data": <updated command> }; DELETE returns { "data": { "id": "clx-demo" } }. GET errors are 401/403/404/500; mutation errors add 400 and 409. Delete can fail when relations or policy make the command ineligible.

PATCH /api/gov/admin/commands/[id]/parent

Auth: GOV_SYSTEM_ADMIN; success: 200.

FieldTypeRequired?Constraints
parentIdstring/nullYes1–255 or null.
curl -X PATCH http://localhost:3000/api/gov/admin/commands/clx-nrl/parent \
  -H 'content-type: application/json' -H 'cookie: next-auth.session-token=SYSTEM_SESSION' \
  -d '{"parentId":"clx-onr"}'

Success returns { "data": <updated command> }. Errors include 400 invalid body/self-parent/cycle, 401/403, 404 missing command/parent, and 500 transaction failure. The operation checks the graph before applying the parent.

Admin users

GET /api/gov/admin/users

Auth: admin-tier government session. Success: 200.

curl http://localhost:3000/api/gov/admin/users -H 'cookie: next-auth.session-token=ADMIN_SESSION'

Response is { "data": [{ "id": "...", "email": "poc.ai@navy.mil", "name": "Dr. Marcus Vela", "role": "GOV_POC", "commandId": "...", "title": "AI/ML Portfolio Lead" }] }. Password hashes are not returned. Errors: 401/403/500.

POST /api/gov/admin/users

Auth: admin-tier policy; assignable roles are validated by the route.

FieldTypeRequired?Constraints
emailemail stringYesTrimmed, max 255.
namestringYesTrimmed, 1–200.
passwordstringYes14–128 and password regex.
roleenumYesAssignable government roles; cannot bypass tier policy.
commandIdstring/nullNo1–255 or null.
titlestring/nullNoMax 200.
curl -X POST http://localhost:3000/api/gov/admin/users \
  -H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
  -d '{"email":"demo.poc@navy.mil","name":"Demo POC","password":"Roundtable2026!Demo","role":"GOV_POC","commandId":"clx-niwcpac","title":"Pilot POC"}'

Success 201 returns a sanitized user shape. Errors: 400 validation or role/command policy, 401/403, 409 duplicate email, 500.

PATCH /api/gov/admin/users

Auth: admin-tier policy.

FieldTypeRequired?Constraints
userIdstringYes1–255.
roleenumNoAssignable role.
commandIdstring/nullNo1–255 or null.
titlestring/nullNoMax 200.
curl -X PATCH http://localhost:3000/api/gov/admin/users \
  -H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
  -d '{"userId":"clx-user","role":"GOV_COMMAND_LEAD","commandId":"clx-navsea","title":"NAVSEA lead"}'

Success is { "data": <sanitized user> }. Errors: 400, 401/403, 404, policy protection for last system admin, and 500. The next session refresh sees changed authorization attributes from Prisma.

Publishing

POST /api/gov/announcements

Auth: government publisher (route helper). Success: 201.

FieldTypeRequired?Constraints
titlestringYesTrimmed, 1–200.
bodystringYesTrimmed, 1–8,000.
curl -X POST http://localhost:3000/api/gov/announcements \
  -H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
  -d '{"title":"ROUNDTABLE pilot update","body":"Industry office hours are available next week."}'

Success { "data": { "id": "clx-announcement" } }. Errors: 400, 401/403, 500. The route creates a published row, audits it, and notifies industry users.

POST /api/gov/announcements/[id]/attachments

Auth: government publisher. Content type: multipart/form-data with one or more files parts. Success: 201.

ConstraintValue
Allowed typesPDF, DOCX, PPTX, TXT, PNG, JPG, or JPEG; validated by filename extension and file contents.
Per-file size15 MB maximum.
Per-announcement count10 attachments maximum.
curl -X POST http://localhost:3000/api/gov/announcements/clx-announcement/attachments \
  -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
  -F 'files=@briefing.pdf'

Success returns { "data": [{ "id", "fileName", "contentType", "sizeBytes", "createdAt" }] }. Errors: 400 for missing files, disallowed type/content, an oversized file, or exceeding the attachment count; 401/403 for the session or publisher gate; 404 for a missing or deleted announcement; 413 for an oversized request body; 500 for a server error. Each upload is audited.

GET /api/gov/announcements/[id]/attachments/[attachmentId]

Auth: active signed-in session. Published announcement files are downloadable by any signed-in user; files on unpublished announcements are restricted to government sessions. Success: 200 with the stored file content type and attachment disposition.

curl http://localhost:3000/api/gov/announcements/clx-announcement/attachments/clx-attachment \
  -H 'cookie: next-auth.session-token=SESSION'

Errors: 401 when no active session is present, 404 for a missing/deleted or unauthorized attachment, and 500 for a server error. Successful downloads are audited. The route returns a generic not-found response for unauthorized records.

DELETE /api/gov/announcements/[id]/attachments/[attachmentId]

Auth: government publisher. Success: 200.

curl -X DELETE http://localhost:3000/api/gov/announcements/clx-announcement/attachments/clx-attachment \
  -H 'cookie: next-auth.session-token=ADMIN_SESSION'

Success returns { "data": { "id": "clx-attachment" } }; errors: 401/403 for the session or publisher gate, 404 for a missing/deleted or mismatched attachment, and 500 for a server error. Deletions are audited.

POST /api/gov/calls

Auth: government publisher. Success: 201.

FieldTypeRequired?Constraints
titlestringYesTrimmed, 1–200.
descriptionstringYesTrimmed, 1–8,000.
techTagsstringNoMax 400; normalized lowercase CSV.
closesAtdate stringNoYYYY-MM-DD.
statusenumNoOPEN, DRAFT, or PENDING.
releasesAtdate-time stringNoRequired as a future time when status is PENDING.
curl -X POST http://localhost:3000/api/gov/calls \
  -H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
  -d '{"title":"Maritime AI for Predictive Maintenance","description":"Share relevant maintenance analytics capabilities.","techTags":"ai/ml,predictive maintenance","closesAt":"2026-04-30"}'

Success { "data": { "id": "clx-call" } }; errors 400, 401/403, 500. An OPEN call is public and notifies industry users; DRAFT and PENDING calls remain government-only. A pending call requires a future releasesAt.

PATCH /api/gov/calls/[id]

Auth: government publisher.

FieldTypeRequired?Constraints
statusenumNoOPEN, CLOSED, DRAFT, or PENDING, subject to the transition rules below.
releasesAtdate-time stringNoFuture date-time required when the resulting status is PENDING; not valid for other resulting statuses.
Content fieldsstring/nullNoDraft and pending calls can update their editable title, description, tags, dates, solicitation, contact, office, and place-of-performance fields.
curl -X PATCH http://localhost:3000/api/gov/calls/clx-call \
  -H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
  -d '{"status":"CLOSED"}'

Allowed status transitions are DRAFT → PENDING, DRAFT → OPEN, PENDING → OPEN, PENDING → DRAFT, OPEN → CLOSED, and CLOSED → OPEN. A pending call whose releasesAt has passed is treated as open for lifecycle actions, so it can be closed but can no longer be edited or returned to draft. Opening a draft immediately or explicitly releasing a draft or pending call notifies industry; scheduling alone and reopening a closed call do not. A pending call that reaches its release time becomes visible at query time without a notification.

Content edits are accepted only while the effective status is DRAFT or PENDING. Status changes and content edits are audited. Success { "data": { "id": "clx-call", "status": "CLOSED" } }; errors 400 for invalid input, transition, or release date; 401/403 for the publisher gate; 404 for a missing call; and 500 for a server error.

Preset tags

GET /api/gov/admin/tags

Auth: GOV_SYSTEM_ADMIN only. Success: 200.

curl http://localhost:3000/api/gov/admin/tags \
  -H 'cookie: next-auth.session-token=SYSTEM_SESSION'

Success returns { "data": [{ "id", "label", "createdAt" }] }, sorted by normalized label. Errors: 401 unauthenticated, 403 authenticated but not a System Admin, and 500 for a server error.

POST /api/gov/admin/tags

Auth: GOV_SYSTEM_ADMIN only. Success: 201.

FieldTypeRequired?Constraints
labelstringYesTrimmed, lowercased, at most 50 characters, matching ^[a-z0-9][a-z0-9 \-\/&.+]{0,49}$.
curl -X POST http://localhost:3000/api/gov/admin/tags \
  -H 'content-type: application/json' \
  -H 'cookie: next-auth.session-token=SYSTEM_SESSION' \
  -d '{"label":"Machine Learning"}'

Success returns { "data": { "id", "label", "createdAt" } }; errors: 400 invalid input, 401 unauthenticated, 403 authenticated but not a System Admin, 409 when the normalized label already exists, and 500 for a server error. Creation is audited.

DELETE /api/gov/admin/tags/[id]

Auth: GOV_SYSTEM_ADMIN only. Success: 200.

curl -X DELETE http://localhost:3000/api/gov/admin/tags/clx-preset-tag \
  -H 'cookie: next-auth.session-token=SYSTEM_SESSION'

Success returns { "data": { "id": "clx-preset-tag" } }; errors: 401 unauthenticated, 403 authenticated but not a System Admin, 404 for a missing tag, and 500 for a server error. Deletion is audited.

Engagements

POST /api/gov/engagements

Auth: requireGovWrite; success 201.

FieldTypeRequired?Constraints
titlestringYesTrimmed, 1–200.
typeenumYesNine EngagementType values.
statusenumYesPLANNED, COMPLETED, FOLLOW_UP, CLOSED.
datedate stringYesYYYY-MM-DD.
locationstringNoMax 200 or empty.
summarystringNoMax 4,000 or empty.
organizationIdsstring arrayConditionallyUp to 25 organization IDs, de-duplicated; every ID must exist.
otherOrganizationstringConditionallyFree-text participant name, max 200.
commandIdstringYesNonempty ID.

At least one of organizationIds (non-empty array) or otherOrganization (non-blank) is required; otherwise the request is rejected with 400. The first selected organization is denormalized into Engagement.organizationId; all selected organizations are written as EngagementOrganization rows.

curl -X POST http://localhost:3000/api/gov/engagements \
  -H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
  -d '{"title":"Pelagic Dynamics autonomy demo","type":"DEMO","status":"PLANNED","date":"2026-03-20","location":"NIWC Pacific","summary":"Review UUV autonomy capability.","organizationIds":["clx-pelagic"],"commandId":"clx-niwcpac"}'

Success { "data": { "id": "clx-engagement" } }. Errors: 400 field/foreign-key validation, 401/403 role/scope, 404 referenced record, 500. Current user becomes createdBy; engagement_created is audited.

PATCH /api/gov/engagements/[id]

Auth: requireGovWrite; request fields are the same fields above, with the same at-least-one-organization rule. The update replaces the engagement's EngagementOrganization rows with the new selection and keeps the denormalized organizationId in sync (preserving the existing primary organization when it is still selected).

curl -X PATCH http://localhost:3000/api/gov/engagements/clx-engagement \
  -H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
  -d '{"title":"Pelagic Dynamics autonomy demo","type":"DEMO","status":"COMPLETED","date":"2026-03-20","location":"NIWC Pacific","summary":"Completed technical demonstration.","organizationIds":["clx-pelagic"],"commandId":"clx-niwcpac"}'

Success { "data": { "id": "clx-engagement" } }; errors 400, 401/403, 404, 500.

DELETE /api/gov/engagements/[id]

Auth: requireGovWrite. No body.

curl -X DELETE http://localhost:3000/api/gov/engagements/clx-engagement \
  -H 'cookie: next-auth.session-token=ADMIN_SESSION'

Success { "data": { "id": "clx-engagement" } }; errors 401/403, 404, 500. Related notes, organization links, and attachments are cascade-deleted with the engagement.

POST /api/gov/engagements/[id]/notes

Auth: requireGovWrite.

FieldTypeRequired?Constraints
bodystringYesTrimmed, 1–4,000.
curl -X POST http://localhost:3000/api/gov/engagements/clx-engagement/notes \
  -H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
  -d '{"body":"Follow up with the autonomy portfolio lead."}'

Success 201 { "data": { "id": "clx-note" } }; errors 400, 401/403, 404, 500.

POST /api/gov/engagements/[id]/attachments

Auth: requireGovWrite plus command scope on the engagement's command. Content type: multipart/form-data with one or more files parts.

ConstraintValue
Allowed typesPDF, DOCX, PPTX, TXT, PNG, JPG/JPEG — validated by extension and magic bytes.
Per-file size15 MB.
Per-engagement total10 attachments, enforced inside a row-locked transaction (SELECT … FOR UPDATE) so concurrent uploads cannot exceed it.
curl -X POST http://localhost:3000/api/gov/engagements/clx-engagement/attachments \
  -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
  -F 'files=@slides.pdf' -F 'files=@minutes.docx'

Success 201 with { "data": [{ "id", "fileName", "contentType", "sizeBytes", "createdAt" }] }. Errors: 400 no files/disallowed type or contents/oversize/over the 10-attachment limit, 401 no session, 403 read-only role or out-of-scope command, 404 missing engagement, 500. Each stored attachment writes an engagement_attachment_uploaded audit event. File bytes are stored in the database (EngagementAttachment.data), not in S3.

GET /api/gov/engagements/[id]/attachments/[attachmentId]

Auth: requireGovRead. Streams the file back with its stored content type and an attachment disposition; the download is audit-logged. Errors: 401, 403 industry session, 404 wrong engagement/attachment pair, 500.

DELETE /api/gov/engagements/[id]/attachments/[attachmentId]

Auth: requireGovWrite plus command scope. Success { "data": { "id": "clx-attachment" } } and an audit event; errors 401/403, 404, 500.

Organization government routes

PATCH /api/gov/organizations/[id]/status

Auth: requireGovWrite.

FieldTypeRequired?Constraints
statusenumYesNEW, UNDER_REVIEW, ACTIVE, WATCHLIST, INACTIVE.
curl -X PATCH http://localhost:3000/api/gov/organizations/clx-pelagic/status \
  -H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
  -d '{"status":"ACTIVE"}'

Success { "data": { "id": "clx-pelagic", "status": "ACTIVE" } }; errors 400, 401/403, 404, 500.

POST /api/gov/organizations/[id]/dialogue

Auth: requireGovWrite.

FieldTypeRequired?Constraints
bodystringYesTrimmed, 1–4,000.
curl -X POST http://localhost:3000/api/gov/organizations/clx-pelagic/dialogue \
  -H 'content-type: application/json' -H 'cookie: next-auth.session-token=ADMIN_SESSION' \
  -d '{"body":"Coordinate a follow-up with the undersea portfolio."}'

Success 201 { "data": { "id": "clx-dialogue-note" } }; errors 400, 401/403, 404, 500. This note is internal government context.

Government status and role summary

HandlerBody/queryPassing audienceTypical denial
Admin command listNoneGovernment list policy401 or 403.
Admin command create/update/deleteJSON command fieldsSystem admin403 for admin or command lead.
Parent reassignmentparentId string/nullSystem admin400 cycle, 403 role, 409 serialization conflict.
Admin users GET/POST/PATCHUser fieldsAdmin tier policy403 tier or protected-account rule.
Announcement createtitle, bodyGovernment publisher403 readonly/POC.
Call create/statusCall fields or statusGovernment publisher403 readonly/POC.
Engagement create/update/deleteEngagement fieldsGovernment writer403 readonly/industry.
Engagement notebodyGovernment writer403 readonly/industry.
Organization statusstatusGovernment writer403 readonly/industry.
Organization dialoguebodyGovernment writer403 readonly/industry.

Error envelope examples

Invalid request:

{"error":"Invalid request"}

Unauthorized:

{"error":"Unauthorized"}

Forbidden:

{"error":"Forbidden"}

Not found:

{"error":"Not found"}

The exact punctuation varies between older portal handlers and shared government helpers, but callers should branch on HTTP status and treat the message as human-readable rather than a stable machine code.

Engagement date handling

The API accepts an ISO calendar date such as 2026-03-20, then stores a JavaScript DateTime. The UI and export code commonly display the date portion. Clients should send a date rather than a locale-formatted string such as 03/20/2026, because Zod's YYYY-MM-DD regular expression rejects locale formats.

Publishing side effects

Publishing an announcement or call has three observable outcomes:

  1. A row is persisted with the current government author or open status.
  2. An audit event records the operation and actor.
  3. Industry Notification rows may be created according to each recipient's settings.

If the database write succeeds but a notification fan-out fails, inspect server logs and the resulting record before retrying. Repeating a publish request can create another publication unless the client first checks current state.

Command safety

The parent endpoint accepts null to detach a command to the top level. It does not accept an empty string as a parent ID because the schema requires a nonempty string when non-null. The serializable transaction re-reads the current parent before writing so the audit detail reflects the value actually replaced.

Request construction notes

Government JSON routes reject malformed JSON before database work. Send content-type: application/json and serialize dates as YYYY-MM-DD. Do not send Prisma enum values with spaces or display labels: use exact values such as INDUSTRY_FORUM, FOLLOW_UP, UNDER_REVIEW, and GOV_SYSTEM_ADMIN.

IDs in examples are deliberately symbolic. Resolve them from a government list response or a seeded database; never infer a cuid from a display name. A route that accepts an ID still verifies that the referenced row exists and, where applicable, that the current role may operate on it.

Audit side effects by route family

FamilyTypical audit event or behavior
Command hierarchyReparent event includes previous and new parent IDs.
User administrationRole/assignment changes are recorded by admin helper.
Organization statusStatus change includes organization and new status.
Dialogue and engagement notesAuthor and parent record are attributable.
Engagement create/updateCreator and operation are recorded.
Announcement/call publicationPublisher and public title are recorded.

The audit event name is more stable than the human-facing success body. Consumers should use HTTP status and their own operation ID rather than scraping an error sentence.

Permission test matrix

Test callerRequestExpected
Industry contactPOST /api/gov/engagements403.
GOV_READONLYPOST /api/gov/announcements403.
GOV_POCPOST /api/gov/calls403 under publisher policy.
lead.navsea@navy.milEdit NSWC DD403 exact-scope denial.
admin@navy.milReparent command403 system-admin-only.
sysadmin@navy.milReparent valid command200 if body and cycle checks pass.
lead.navsea@navy.milExport CSV403 enterprise-export gate.
admin@navy.milExport CSV200.

Use disposable records for mutation tests. A 403 is evidence of the gate only when the request reached the intended route and the session identity is the account described.