BinktermPHP API Documentation

July 18, 2026 · View on GitHub

Authentication

Most endpoints require session authentication. Log in via POST /api/auth/login to receive a session cookie (binktermphp_session). Include this cookie in subsequent requests. Some endpoints also require a CSRF token returned at login; include it as X-CSRF-Token on state-changing requests.

Quickstart

1. Log in

POST /api/auth/login
Content-Type: application/json

{"username": "youruser", "password": "yourpassword"}

Response:

{
  "success": true,
  "csrf_token": "abc123...",
  "user": { "id": 1, "username": "youruser", "is_admin": false }
}

The response also sets a binktermphp_session cookie. Include it in all subsequent requests.

2. Make an authenticated request

GET /api/messages/echomail?area_id=1&limit=25
Cookie: binktermphp_session=<session-cookie>

For state-changing requests (POST, PUT, DELETE), also include the CSRF token:

POST /api/messages/echomail
Cookie: binktermphp_session=<session-cookie>
X-CSRF-Token: abc123...
Content-Type: application/json

{"area_id": 1, "subject": "Hello", "body": "Message body"}

Error responses use a structured format:

{
  "error": "Invalid credentials",
  "error_code": "errors.auth.invalid_credentials"
}

Contents


Public API

Account

MethodPathAuthSummary
POST/api/account/reminderNoSend account reminder email to inactive user.

POST /api/account/reminder

Public

Sends a reminder message to a user who has not yet logged in. Validates that the user exists and has not logged in before sending. Returns success with email_sent flag indicating whether email delivery was attempted.

Request Body (JSON)

Reminder request data

FieldTypeRequiredDescription
usernamestringYesUsername to send reminder to

Response (JSON)

Reminder send result

FieldTypeDescription
successbooleanWhether reminder was sent
message_codestringLocalization key for result message
email_sentbooleanWhether email was actually sent

Error Responses

StatusDescription
400Missing username or reminder send failed
404User not found or already logged in
500Server error sending reminder

Address Book

MethodPathAuthSummary
GET/api/address-book/YesList user's address book entries with optional search filter.
GET/api/address-book/{id}YesRetrieve a specific address book entry by ID.
POST/api/address-book/YesCreate a new address book entry.
PUT/api/address-book/{id}YesUpdate an existing address book entry.
DELETE/api/address-book/{id}YesDelete an address book entry.
GET/api/address-book/search/{query}YesSearch address book entries plus matching local users for autocomplete.
GET/api/address-book/statsYesGet address book statistics for the user.
POST/api/address-book/import-from-keyserverYesImport a local PGP keyserver key into the address book (update or create).

GET /api/address-book/

Requires authentication

Retrieves all address book entries for the authenticated user. Supports optional full-text search via the 'search' query parameter to filter entries by name or address. Returns an array of matching entries.

Query Parameters

NameTypeRequiredDescription
searchstringNoOptional search term to filter entries by name or address

Response (JSON)

Array of address book entries matching the search criteria

FieldTypeDescription
successbooleanAlways true on success
entriesarrayArray of address book entry objects
entries[].idintegerEntry ID
entries[].namestringContact display name
entries[].messaging_user_idinteger|nullBBS user ID if linked to a local user
entries[].node_addressstring|nullFTN node address
entries[].emailstring|nullEmail address
entries[].descriptionstring|nullFree-text notes
entries[].always_crashmailbooleanAlways send crashmail to this address
entries[].pgp_contact_key_idinteger|nullLinked saved correspondent-key ID
entries[].pgp_key_fingerprintstring|nullLinked correspondent-key fingerprint
entries[].pgp_key_user_id_stringstring|nullLinked correspondent-key user ID
entries[].pgp_key_labelstring|nullLinked correspondent-key label
entries[].created_atstringISO 8601 creation timestamp
entries[].updated_atstringISO 8601 last-update timestamp

Error Responses

StatusDescription
500Failed to load address book entries

GET /api/address-book/{id}

Requires authentication

Fetches a single address book entry by its ID, verifying ownership by the authenticated user. Returns the full entry details or 404 if not found or not owned by the user.

Path Parameters

NameTypeDescription
idintegerAddress book entry ID

Response (JSON)

Single address book entry object

FieldTypeDescription
successbooleanTrue if entry found
entryobjectAddress book entry details
entry.idintegerEntry ID
entry.namestringContact display name
entry.messaging_user_idinteger|nullBBS user ID if linked to a local user
entry.node_addressstring|nullFTN node address
entry.emailstring|nullEmail address
entry.descriptionstring|nullFree-text notes
entry.always_crashmailbooleanAlways send crashmail to this address
entry.pgp_contact_key_idinteger|nullLinked saved correspondent-key ID
entry.pgp_key_fingerprintstring|nullLinked correspondent-key fingerprint
entry.pgp_key_user_id_stringstring|nullLinked correspondent-key user ID
entry.pgp_key_labelstring|nullLinked correspondent-key label
entry.pgp_armored_public_keystring|nullLinked correspondent ASCII-armored public key
entry.created_atstringISO 8601 creation timestamp
entry.updated_atstringISO 8601 last-update timestamp

Error Responses

StatusDescription
404Entry not found or not owned by user
500Failed to load address book entry

POST /api/address-book/

Requires authentication

Creates a new address book entry for the authenticated user. Accepts entry data in JSON request body. Returns the newly created entry ID on success. Validates user authentication and required fields; throws AddressBookException for validation errors.

Request Body (JSON)

Address book entry data

FieldTypeRequiredDescription
namestringYesContact name
messaging_user_idstringYesUser ID or handle used for FTN messaging
node_addressstringYesFTN destination address
emailstringNoReference email address
descriptionstringNoFree-text notes
always_crashmailbooleanNoWhether compose should default this contact to crashmail
pgp_public_keystringNoASCII-armored correspondent public key to save and link to the contact

Response (JSON)

Newly created entry confirmation

FieldTypeDescription
successbooleanTrue on successful creation
entry_idintegerID of the newly created entry
message_codestringLocalization key for UI message

Error Responses

StatusDescription
400User ID not found, validation error, or creation failed

PUT /api/address-book/{id}

Requires authentication

Updates an address book entry by ID, verifying ownership by the authenticated user. Accepts partial or full entry data in JSON request body. Returns success confirmation or error if entry not found or update fails.

Path Parameters

NameTypeDescription
idintegerAddress book entry ID to update

Request Body (JSON)

Updated address book entry data (partial updates supported)

FieldTypeRequiredDescription
namestringNoContact name
messaging_user_idstringNoUser ID or handle used for FTN messaging
node_addressstringNoFTN destination address
emailstringNoReference email address
descriptionstringNoFree-text notes
always_crashmailbooleanNoWhether compose should default this contact to crashmail
pgp_public_keystringNoASCII-armored correspondent public key to save and link to the contact; submit an empty string to unlink

Response (JSON)

Update confirmation

FieldTypeDescription
successbooleanTrue if update succeeded
message_codestringLocalization key for UI message

Error Responses

StatusDescription
400Update failed or validation error
404Entry not found (via AddressBookException)

DELETE /api/address-book/{id}

Requires authentication

Deletes an address book entry by ID, verifying ownership by the authenticated user. Returns success confirmation or 404 if entry not found or not owned by user.

Path Parameters

NameTypeDescription
idintegerAddress book entry ID to delete

Response (JSON)

Deletion confirmation

FieldTypeDescription
successbooleanTrue if deletion succeeded
message_codestringLocalization key for UI message

Error Responses

StatusDescription
404Entry not found or not owned by user
500Failed to delete address book entry

GET /api/address-book/search/{query}

Requires authentication

Performs an autocomplete search for the authenticated user. Results include the user's address book entries plus matching local BBS users by real name or username, returned in a shared entry shape suitable for compose UI pickers. Limits results to 10 by default, maximum 20. Query string is URL-decoded before search.

Path Parameters

NameTypeDescription
querystringSearch query string (URL-encoded)

Query Parameters

NameTypeRequiredDescription
limitintegerNoMaximum results to return (default 10, max 20)

Response (JSON)

Array of matching autocomplete entries

FieldTypeDescription
successbooleanTrue on success
entriesarrayMatching address-book and local-user entries (limited)
entries[].idintegerEntry ID
entries[].namestringContact display name
entries[].messaging_user_idinteger|nullBBS user ID if linked to a local user
entries[].node_addressstring|nullFTN node address
entries[].emailstring|nullEmail address
entries[].descriptionstring|nullFree-text notes
entries[].always_crashmailbooleanAlways send crashmail to this address
entries[].pgp_contact_key_idinteger|nullLinked saved correspondent-key ID
entries[].pgp_key_fingerprintstring|nullLinked correspondent-key fingerprint
entries[].pgp_key_user_id_stringstring|nullLinked correspondent-key user ID
entries[].pgp_key_labelstring|nullLinked correspondent-key label
entries[].created_atstringISO 8601 creation timestamp
entries[].updated_atstringISO 8601 last-update timestamp
entries[].node_system_namestring|nullSystem name from nodelist (search results only)
entries[].node_domainstring|nullNetwork domain from nodelist (search results only)

Error Responses

StatusDescription
500Failed to search address book entries

GET /api/address-book/stats

Requires authentication

Retrieves aggregate statistics about the authenticated user's address book, such as total entry count or other metrics. Useful for UI display or analytics.

Response (JSON)

Address book statistics object

FieldTypeDescription
successbooleanTrue on success
statsobjectStatistics object
stats.total_entriesintegerTotal number of entries in the address book
stats.entries_with_emailintegerNumber of entries that have an email address
stats.entries_with_descriptionintegerNumber of entries that have a description

Error Responses

StatusDescription
500Failed to load address book statistics

POST /api/address-book/import-from-keyserver

Requires authentication

Imports a PGP key into the address book by fingerprint. If the authenticated user has an existing address book entry whose messaging_user_id matches the key owner's username and that entry has no PGP key set, the key is linked automatically. Otherwise, the key data is returned so the caller can present a creation form.

Local keys are fetched from the BBS's own PGP key table. When source_address is provided and the key is not found locally, the endpoint attempts to retrieve the armored public key from the remote BBS at that FTN address.

Request Body (JSON)

FieldTypeDescription
fingerprintstring40-character PGP key fingerprint to import
source_addressstringOptional FTN address (zone:net/node) or hostname of the BBS that published this key; used to fetch the armored key for remote results and pre-fill the node address in the creation form
usernamestringOptional BBS username shown in the keyserver index result; used as a fallback when the armored-key fetch (remote op=get) does not carry a username

Response (JSON) — action updated

Returned when an existing address book entry was found and the PGP key was linked automatically.

FieldTypeDescription
successbooleanTrue
actionstring"updated"
entry_idintegerID of the updated address book entry
entry_namestringDisplay name of the updated address book entry

Response (JSON) — action needs_create

Returned when no matching address book entry was found; the caller should present a creation form using the returned key data.

FieldTypeDescription
successbooleanTrue
actionstring"needs_create"
key_dataobjectDetails of the PGP key
key_data.fingerprintstringKey fingerprint
key_data.armored_public_keystringASCII-armored public key block
key_data.usernamestringBBS username of the key owner
key_data.real_namestringReal name of the key owner
key_data.user_id_stringstringPGP user ID string
key_data.key_algorithmstringKey algorithm (e.g. RSA4096)
key_data.suggested_node_addressstringPre-filled FTN node address when source_address was an FTN address; empty string otherwise

Error Responses

StatusDescription
400Fingerprint missing or invalid
404PGP key not found locally or remotely, or PGP is disabled
409Matching address book entry already has a PGP key set
500Failed to update address book entry

Ads

MethodPathAuthSummary
POST/api/ads/{id}/impressionYesRecord an advertisement impression for the authenticated user.
POST/api/ads/{id}/clickYesRecord an advertisement click and retrieve the click URL.

POST /api/ads/{id}/impression

Requires authentication

Logs that the authenticated user has viewed an advertisement. Used for tracking ad impressions and analytics. Returns success confirmation or error if recording fails.

Path Parameters

NameTypeDescription
idintegerAdvertisement ID

Response (JSON)

Impression recording confirmation

FieldTypeDescription
successbooleanImpression was recorded

Error Responses

StatusDescription
500Failed to record impression

POST /api/ads/{id}/click

Requires authentication

Logs that the authenticated user clicked an advertisement and returns the target click URL. Used for tracking ad engagement and redirecting users to advertiser destinations.

Path Parameters

NameTypeDescription
idintegerAdvertisement ID

Response (JSON)

Click recording confirmation with redirect URL

FieldTypeDescription
successbooleanClick was recorded
click_urlstringURL to redirect user to (advertiser destination)

Error Responses

StatusDescription
404Advertisement not found
500Failed to record click

Auth

MethodPathAuthSummary
POST/api/auth/loginNoAuthenticate user with username and password, returning session cookie and CSRF token.
POST/api/auth/logoutNoInvalidate user session and clear authentication cookie.
POST/api/auth/verify-gateway-tokenNoVerify gateway token for external service integration (requires API key).
POST/api/auth/gateway-tokenYesGenerate a time-limited gateway token for authenticated user.
POST/api/auth/forgot-passwordNoInitiate password reset by username or email address.
POST/api/auth/validate-reset-tokenNoValidate password reset token before allowing password change.
POST/api/auth/reset-passwordNoComplete password reset with valid token and new password.

POST /api/auth/login

Public

Validates credentials and creates an authenticated session. Sets a 30-day HTTP-only session cookie and tracks the login event. Returns a CSRF token for subsequent authenticated requests. The service parameter (default 'web') determines session behavior. Failed authentication returns 401 with invalid credentials error.

Request Body (JSON)

Login credentials

FieldTypeRequiredDescription
usernamestringYesUser login name
passwordstringYesUser password
servicestringNoService identifier (default: 'web')

Response (JSON)

Authentication success response

FieldTypeDescription
successbooleanAlways true on success
csrf_tokenstringnull

Error Responses

StatusDescription
400Missing username or password
401Invalid credentials

POST /api/auth/logout

Public

Terminates the current session by removing the session cookie and invalidating the session in the database. Safe to call even if no session exists. Always returns success regardless of prior session state.

Response (JSON)

Logout confirmation

FieldTypeDescription
successbooleanAlways true

POST /api/auth/verify-gateway-token

Public

Validates a gateway token issued for external services like bbslinkgateway. Requires X-API-KEY header matching BBSLINK_API_KEY environment variable. Returns user information if token is valid and not expired. Used by external systems to authenticate users without direct password access.

Request Body (JSON)

Token verification request

FieldTypeRequiredDescription
useridintegerYesUser ID (accepts userid or user_id)
tokenstringYesGateway token to verify

Response (JSON)

Token validation result with user information

FieldTypeDescription
validbooleanToken validity status
userInfoobjectUser information object if valid (absent when valid is false)
userInfo.user_idintegerBBS user ID
userInfo.usernamestringUsername
userInfo.doorstring|nullDoor/service identifier the token was issued for

Error Responses

StatusDescription
401Invalid or missing API key
400Missing userid or token, or invalid/expired token

POST /api/auth/gateway-token

Requires authentication

Creates a gateway token for the authenticated user to access external services. TTL is capped at 10 minutes maximum for security. Optional door parameter can specify which service the token grants access to. Returns the token and expiration time in seconds.

Request Body (JSON)

Token generation parameters

FieldTypeRequiredDescription
doorstringnullNo
ttlintegerNoTime-to-live in seconds (default: 300, max: 600)

Response (JSON)

Generated gateway token

FieldTypeDescription
successbooleanAlways true
useridintegerUser ID
tokenstringGateway token string
expires_inintegerToken expiration time in seconds

Error Responses

StatusDescription
401Authentication required

POST /api/auth/forgot-password

Public

Requests a password reset for a user identified by username or email. Triggers password reset email with a time-limited token. Response indicates success or provides localized error details. Does not reveal whether username/email exists for security.

Request Body (JSON)

Password reset request

FieldTypeRequiredDescription
usernameOrEmailstringYesUsername or email address

Response (JSON)

Password reset request result

FieldTypeDescription
successbooleanRequest success status

Error Responses

StatusDescription
400Missing username or email

POST /api/auth/validate-reset-token

Public

Checks if a password reset token is valid and not expired. Used to verify token before presenting password reset form. Returns validity status without revealing token details.

Request Body (JSON)

Token validation request

FieldTypeRequiredDescription
tokenstringYesPassword reset token

Response (JSON)

Token validity status

FieldTypeDescription
validbooleanToken validity status

Error Responses

StatusDescription
400Missing token or invalid/expired token

POST /api/auth/reset-password

Public

Resets user password using a valid reset token. Token must pass validation before calling this endpoint. Returns success status with localized error messages on failure. Sets HTTP 400 status if reset fails.

Request Body (JSON)

Password reset completion

FieldTypeRequiredDescription
tokenstringYesValid password reset token
newPasswordstringYesNew password for user

Response (JSON)

Password reset result

FieldTypeDescription
successbooleanReset success status

Error Responses

StatusDescription
400Missing token/password or invalid/expired token

Binkp

MethodPathAuthSummary
GET/api/binkp/statusYesGet current BinkP daemon status (admin only).
POST/api/binkp/pollYesTrigger BinkP poll for a specific address or all uplinks.
POST/api/binkp/poll-allYesTrigger BinkP poll for all configured uplinks.
POST/api/binkp/process-packetsYesTrigger packet processing for BinkP protocol.
GET/api/binkp/uplinksYesRetrieve list of configured BinkP uplinks.
GET/api/binkp/uplink-statusYesTest BinkP uplink authentication and connectivity.
POST/api/binkp/uplinksYesAdd a new BinkP uplink configuration.
PUT/api/binkp/uplinks/{address}YesUpdate an existing BinkP uplink configuration.
DELETE/api/binkp/uplinks/{address}YesDelete a BinkP uplink configuration.
GET/api/binkp/files/inboundYesList files in BinkP inbound directory.
GET/api/binkp/files/outboundYesRetrieve list of outbound files queued for transmission.
POST/api/binkp/process/inboundYesTrigger processing of inbound BinkP packets.
POST/api/binkp/process/outboundYesTrigger outbound queue processing and polling.
GET/api/binkp/kept-packets/inspectYesInspect contents of a kept inbound or outbound packet.
GET/api/binkp/kept-packets/downloadYesDownload a kept inbound or outbound packet file.
GET/api/binkp/queue/inspectYesInspect contents of a queued inbound or outbound packet.
GET/api/binkp/queue/downloadYesDownload a queued inbound or outbound packet file.
GET/api/binkp/kept-packets/bundle/listYesList contents of a packet bundle (archive file).
GET/api/binkp/kept-packets/bundle/inspectYesInspect contents of a kept BinkP packet bundle.
GET/api/binkp/kept-packets/bundle/downloadYesDownload a kept BinkP bundle file.
GET/api/binkp/kept-packetsYesList kept BinkP packet bundles.
GET/api/binkp/logsYesRetrieve recent BinkP logs.
GET/api/binkp/logs/searchYesSearch BinkP logs by query string.

GET /api/binkp/status

Requires authentication

Returns operational status of the BinkP daemon including connection state, queue info, and other metrics. Requires admin privileges. Delegates to BinkpController for status retrieval.

Response (JSON)

BinkP daemon operational status

FieldTypeDescription
systemobjectStatic system configuration
system.addressstringFidoNet address of this node
system.sysopstringSysop name
system.locationstringSystem location string
system.hostnamestringBinkP listen hostname
system.portintegerBinkP listen port
scheduleobjectMap of uplink address → schedule status entry
schedule[addr].addressstringUplink FidoNet address
schedule[addr].schedulestringCron-style poll schedule expression
schedule[addr].enabledbooleanWhether this uplink is enabled
schedule[addr].last_pollstringISO 8601 UTC timestamp of last poll, or "Never"
schedule[addr].next_pollstringISO 8601 UTC timestamp of next scheduled poll, or "Unknown"
schedule[addr].due_nowbooleanWhether a poll is currently due
queuesobjectQueue statistics
queues.inbound.pending_filesintegerPackets awaiting processing in the inbound directory
queues.inbound.error_filesintegerFiles in the inbound error directory
queues.inbound.last_checkstringTimestamp of last inbound queue check
queues.outbound.pending_filesintegerPackets queued for outbound transmission
queues.outbound.total_sizeintegerTotal byte size of outbound packets
queues.outbound.total_messagesintegerTotal message count across outbound packets
queues.outbound.last_checkstringTimestamp of last outbound queue check
timestampstringISO 8601 UTC timestamp of when this status was generated

Error Responses

StatusDescription
401Authentication required
403Admin access required
500Failed to retrieve BinkP status

POST /api/binkp/poll

Requires authentication

Initiates an immediate BinkP poll via the admin daemon. If no address is provided, polls all configured uplinks. Requires admin privileges. Returns success confirmation and poll result from the daemon.

Request Body (JSON)

Poll target specification

FieldTypeRequiredDescription
addressstringNoFidoNet address to poll (e.g., '1:123/456'). If omitted, polls all uplinks.

Response (JSON)

Poll trigger confirmation

FieldTypeDescription
successbooleanTrue if poll was triggered
message_codestringLocalization key for UI message
resultobjectDaemon process result
result.exit_codeintegerDaemon exit code (0 = success)
result.stdoutstringStandard output from daemon process
result.stderrstringStandard error output from daemon process

Error Responses

StatusDescription
401Authentication required
403Admin access required
500Failed to trigger BinkP poll

POST /api/binkp/poll-all

Requires authentication

Initiates an immediate poll of all BinkP uplinks via the admin daemon. Convenience endpoint equivalent to POST /api/binkp/poll with no address parameter. Requires admin privileges.

Response (JSON)

Poll trigger confirmation

FieldTypeDescription
successbooleanTrue if poll was triggered
message_codestringLocalization key for UI message
resultobjectDaemon process result
result.exit_codeintegerDaemon exit code (0 = success)
result.stdoutstringStandard output from daemon process
result.stderrstringStandard error output from daemon process

Error Responses

StatusDescription
401Authentication required
403Admin access required
500Failed to poll all BinkP uplinks

POST /api/binkp/process-packets

Requires authentication

Initiates asynchronous processing of BinkP packets via the admin daemon. Requires BinkP administrator privileges. Returns immediately with a success status and processing result. Use this to manually trigger packet queue processing outside normal scheduled intervals.

Response (JSON)

Processing initiation status with result details.

FieldTypeDescription
successbooleanAlways true on success
message_codestringLocalization key: 'ui.api.binkp.process_packets_started'
resultobjectDaemon processing result details
result.exit_codeintegerDaemon exit code (0 = success)
result.stdoutstringStandard output from packet processor
result.stderrstringStandard error output from packet processor

Error Responses

StatusDescription
500Packet processing failed; daemon communication error

Requires authentication

Fetches all configured BinkP uplink nodes. Requires BinkP administrator privileges. Returns uplink configuration details including addresses, authentication settings, and connection parameters.

Response (JSON)

Array of uplink configurations.

FieldTypeDescription
[array]arrayArray of uplink configuration objects
[].addressstringFidoNet address of the uplink (e.g. 1:234/567)
[].mestringLocal address to present to this uplink
[].domainstringFTN domain name (e.g. fidonet)
[].networksarray of stringsAdditional network names served by this uplink
[].hostnamestringHostname or IP address
[].portintegerTCP port (default 24554)
[].passwordstringBinkP session password
[].pkt_passwordstringFTS-0001 packet password
[].tic_passwordstringTIC file password
[].areafix_passwordstringAreaFix robot password
[].filefix_passwordstringFileFix robot password
[].enabledbooleanWhether this uplink is enabled
[].defaultbooleanWhether this is the default uplink
[].send_domain_in_addrbooleanWhether to include domain in the presented address

Error Responses

StatusDescription
500Failed to retrieve uplinks

Requires authentication

Validates authentication credentials and connection status for a specific uplink address. Requires BinkP administrator privileges. Useful for diagnosing uplink configuration issues before enabling production traffic.

Query Parameters

NameTypeRequiredDescription
addressstringYesFidoNet address of uplink to test (e.g., '1:234/567')

Response (JSON)

Uplink authentication and status test results.

FieldTypeDescription
authenticatedbooleanWhether credentials are valid
connectedbooleanWhether connection succeeded

Error Responses

StatusDescription
400Address parameter missing or empty
500Status check failed

POST /api/binkp/uplinks

Requires authentication

Creates a new uplink node configuration. Requires BinkP administrator privileges. Accepts JSON payload with uplink details (address, credentials, connection parameters). Returns created uplink configuration with validation results.

Request Body (JSON)

Uplink configuration parameters.

FieldTypeRequiredDescription
addressstringYesFidoNet address (e.g., '1:234/567')
passwordstringYesBinkP session password
hoststringNoHostname or IP address
portintegerNoTCP port (default 24554)

Response (JSON)

Uplink creation confirmation.

FieldTypeDescription
successbooleanUplink created successfully
message_codestringLocalization key: ui.api.binkp.uplink_added

PUT /api/binkp/uplinks/{address}

Requires authentication

Modifies settings for a configured uplink. Requires BinkP administrator privileges. Accepts JSON payload with updated parameters. Address in URL path identifies the uplink to modify.

Path Parameters

NameTypeDescription
addressstringFidoNet address of uplink to update

Request Body (JSON)

Updated uplink configuration fields.

FieldTypeRequiredDescription
passwordstringNoNew BinkP session password
hoststringNoNew hostname or IP
portintegerNoNew TCP port

Response (JSON)

Update confirmation.

FieldTypeDescription
successbooleanUpdate completed
message_codestringLocalization key: ui.api.binkp.uplink_updated

DELETE /api/binkp/uplinks/{address}

Requires authentication

Removes an uplink node configuration. Requires BinkP administrator privileges. Address in URL path identifies the uplink to delete. Deletion is permanent.

Path Parameters

NameTypeDescription
addressstringFidoNet address of uplink to remove

Response (JSON)

Deletion confirmation.

FieldTypeDescription
successbooleanUplink deleted successfully

GET /api/binkp/files/inbound

Requires authentication

Retrieves inventory of files received via BinkP in the inbound directory. Requires authentication. Returns file metadata including names, sizes, and timestamps for received packets and attachments.

Response (JSON)

Array of inbound files.

FieldTypeDescription
successbooleanOperation success flag
pendingarrayArray of files in the inbound queue awaiting processing
pending[].filenamestringPacket filename
pending[].sizeintegerFile size in bytes
pending[].modifiedstringLast modified timestamp (YYYY-MM-DD HH:MM:SS)
errorsarrayArray of files in the error queue
errors[].filenamestringPacket filename
errors[].sizeintegerFile size in bytes
errors[].modifiedstringLast modified timestamp (YYYY-MM-DD HH:MM:SS)

Error Responses

StatusDescription
500Failed to retrieve inbound files

GET /api/binkp/files/outbound

Requires authentication

Fetches all files currently queued in the outbound directory awaiting BinkP transmission. Returns a JSON array of file metadata. Requires authentication.

Response (JSON)

Array of outbound file objects with metadata

FieldTypeDescription
successbooleanOperation success flag
filesarrayArray of queued outbound packet files
files[].filenamestringPacket filename
files[].sizeintegerFile size in bytes
files[].createdstringFile creation timestamp (YYYY-MM-DD HH:MM:SS)
files[].modifiedstringFile last modified timestamp (YYYY-MM-DD HH:MM:SS)
files[].pathstringFull filesystem path to the packet file
files[].message_countintegerNumber of FTN messages contained in the packet
files[].dest_addressstringDestination FTN address parsed from packet header
files[].orig_addressstringOrigin FTN address parsed from packet header

Error Responses

StatusDescription
500Failed to retrieve outbound files

POST /api/binkp/process/inbound

Requires authentication

Initiates immediate processing of received inbound packets through the daemon. Requires BinkP admin privileges. Returns processing result details. This is an administrative action that may take time to complete.

Response (JSON)

Processing completion status with result details

FieldTypeDescription
successbooleanWhether processing completed successfully
message_codestringLocalization key for UI message
resultobjectProcessing result details from daemon
result.exit_codeintegerDaemon exit code (0 = success)
result.stdoutstringStandard output from packet processor
result.stderrstringStandard error output from packet processor

Error Responses

StatusDescription
403User lacks BinkP admin privileges
500Packet processing failed

POST /api/binkp/process/outbound

Requires authentication

Initiates BinkP poll of all configured nodes to transmit queued outbound packets. Requires BinkP admin privileges. Polls all nodes in the system for transmission opportunities.

Response (JSON)

Polling completion status with result details

FieldTypeDescription
successbooleanWhether polling completed successfully
message_codestringLocalization key for UI message
resultobjectPolling result details from daemon
result.exit_codeintegerDaemon exit code (0 = spawned successfully)
result.stdoutstringStandard output (empty for async spawned poll)
result.stderrstringStandard error output (empty for async spawned poll)

Error Responses

StatusDescription
403User lacks BinkP admin privileges
500Outbound processing failed

GET /api/binkp/kept-packets/inspect

Requires authentication

Examines the structure and contents of archived packets stored in the kept-packets directory. Requires BinkP admin privileges and valid license. Returns detailed packet metadata and message information.

Query Parameters

NameTypeRequiredDescription
typestringNoPacket type: 'inbound' or 'outbound' (default: 'inbound')
datestringNoArchive date directory (format varies by storage)
filenamestringYesPacket filename to inspect

Response (JSON)

Packet inspection details including structure and contents

FieldTypeDescription
successbooleanOperation success flag
packetobjectFTS-0001 packet header metadata
packet.orig_addressstringOrigin FTN address from packet header
packet.dest_addressstringDestination FTN address from packet header
packet.createdstringPacket creation timestamp from header
packet.has_passwordbooleanWhether packet has a non-empty password field
packet.packet_versionintegerFTS-0001 packet version number
packet.product_codestringHex product code from packet header
packet.file_sizeintegerPacket file size in bytes
messagesarrayArray of message headers parsed from the packet
messages[].fromstringSender name
messages[].tostringRecipient name
messages[].subjectstringMessage subject
messages[].datestringMessage date string from packet header
messages[].orig_addrstringOrigin net:node address
messages[].dest_addrstringDestination net:node address
messages[].flagsarray of stringsFTS-0001 attribute flag labels (e.g. Pvt, Crash, Rcvd)
messages[].costintegerMessage cost field

Error Responses

StatusDescription
400Invalid type or missing filename parameter
403User lacks BinkP admin privileges or license not valid

GET /api/binkp/kept-packets/download

Requires authentication

Retrieves and downloads an archived packet file from the kept-packets directory. Requires BinkP admin privileges and valid license. Returns binary packet data with appropriate headers for file download.

Query Parameters

NameTypeRequiredDescription
typestringNoPacket type: 'inbound' or 'outbound' (default: 'inbound')
datestringNoArchive date directory (format varies by storage)
filenamestringYesPacket filename to download

Response (JSON)

Binary packet file data

FieldTypeDescription
file_contentbinaryRaw packet file bytes

Error Responses

StatusDescription
400Invalid type or missing filename parameter
403User lacks BinkP admin privileges or license not valid
404Packet file not found

GET /api/binkp/queue/inspect

Requires authentication

Examines the structure and contents of packets currently in the active queue (not archived). Requires BinkP admin privileges and valid license. Returns detailed packet metadata and message information.

Query Parameters

NameTypeRequiredDescription
typestringNoPacket type: 'inbound' or 'outbound' (default: 'inbound')
filenamestringYesQueue packet filename to inspect

Response (JSON)

Queue packet inspection details including structure and contents

FieldTypeDescription
successbooleanOperation success flag
packetobjectFTS-0001 packet header metadata
packet.orig_addressstringOrigin FTN address from packet header
packet.dest_addressstringDestination FTN address from packet header
packet.createdstringPacket creation timestamp from header
packet.has_passwordbooleanWhether packet has a non-empty password field
packet.packet_versionintegerFTS-0001 packet version number
packet.product_codestringHex product code from packet header
packet.file_sizeintegerPacket file size in bytes
messagesarrayArray of message headers parsed from the packet
messages[].fromstringSender name
messages[].tostringRecipient name
messages[].subjectstringMessage subject
messages[].datestringMessage date string from packet header
messages[].orig_addrstringOrigin net:node address
messages[].dest_addrstringDestination net:node address
messages[].flagsarray of stringsFTS-0001 attribute flag labels (e.g. Pvt, Crash, Rcvd)
messages[].costintegerMessage cost field

Error Responses

StatusDescription
400Invalid type or missing filename parameter
403User lacks BinkP admin privileges or license not valid

GET /api/binkp/queue/download

Requires authentication

Retrieves and downloads a packet file from the active queue directory. Requires BinkP admin privileges and valid license. Returns binary packet data with appropriate headers for file download.

Query Parameters

NameTypeRequiredDescription
typestringNoPacket type: 'inbound' or 'outbound' (default: 'inbound')
filenamestringYesQueue packet filename to download

Response (JSON)

Binary packet file data

FieldTypeDescription
file_contentbinaryRaw packet file bytes

Error Responses

StatusDescription
400Invalid type or missing filename parameter
403User lacks BinkP admin privileges or license not valid
404Queue packet file not found

GET /api/binkp/kept-packets/bundle/list

Requires authentication

Enumerates files contained within a bundle or archive packet from the kept-packets directory. Requires BinkP admin privileges and valid license. Returns list of bundled files with metadata.

Query Parameters

NameTypeRequiredDescription
typestringNoPacket type: 'inbound' or 'outbound' (default: 'inbound')
datestringNoArchive date directory (format varies by storage)
filenamestringYesBundle/archive packet filename

Response (JSON)

List of .pkt files contained in the bundle

FieldTypeDescription
successbooleanOperation success flag
bundlestringBundle filename
bundle_sizeintegerBundle file size in bytes
packetsarrayArray of .pkt files found inside the bundle
packets[].filenamestringPacket filename within the bundle
packets[].sizeintegerUncompressed packet size in bytes

Error Responses

StatusDescription
400Invalid type or missing filename parameter
403User lacks BinkP admin privileges or license not valid

GET /api/binkp/kept-packets/bundle/inspect

Requires authentication

Retrieves detailed inspection data for a specific packet within a kept bundle (inbound or outbound). Requires BinkP admin privileges and valid license. Returns structured packet metadata and contents.

Query Parameters

NameTypeRequiredDescription
typestringNoBundle type: 'inbound' or 'outbound' (default: 'inbound')
datestringNoDate identifier for the bundle
bundlestringYesBundle identifier
pktstringYesPacket filename to inspect

Response (JSON)

Parsed FTS-0001 packet header and per-message header list

FieldTypeDescription
successbooleanTrue on success
packetobjectPacket-level header fields
packet.orig_addressstringOriginating FidoNet address (zone:net/node.point)
packet.dest_addressstringDestination FidoNet address
packet.createdstringPacket creation timestamp as YYYY-MM-DD HH:MM:SS
packet.has_passwordbooleanWhether a session password was set in the packet header
packet.packet_versionintegerFTS-0001 packet version field value
packet.product_codestringTwo-character hex product code
packet.file_sizeintegerPacket file size in bytes
messagesarrayPer-message header entries (up to 1000)
messages[].fromstringSender name (CP437 decoded)
messages[].tostringRecipient name (CP437 decoded)
messages[].subjectstringMessage subject (CP437 decoded)
messages[].datestringMessage date string from packet header
messages[].orig_addrstringOriginating net/node address
messages[].dest_addrstringDestination net/node address
messages[].flagsarrayAttribute flag labels (e.g. ["Pvt"], ["Crash", "Local"])
messages[].costintegerMessage cost field

Error Responses

StatusDescription
400Invalid type, missing bundle or pkt parameter
403License not valid or user lacks BinkP admin privileges

GET /api/binkp/kept-packets/bundle/download

Requires authentication

Downloads a file from a kept bundle as an attachment. Requires BinkP admin privileges and valid license. Returns the file with appropriate headers for binary download.

Query Parameters

NameTypeRequiredDescription
typestringNoBundle type: 'inbound' or 'outbound' (default: 'inbound')
datestringNoDate identifier for the bundle
filenamestringYesFilename to download

Response (binary)

Raw bundle file bytes with download headers

HeaderValue
Content-Typeapplication/octet-stream
Content-LengthFile size in bytes
Content-Dispositionattachment; filename="<bundle_filename>"

Error Responses

StatusDescription
400Invalid type or missing filename parameter
403License not valid or user lacks BinkP admin privileges
404File not found

GET /api/binkp/kept-packets

Requires authentication

Retrieves a list of kept packet bundles (inbound or outbound). Requires BinkP admin privileges and valid license. Useful for browsing archived or retained packets.

Query Parameters

NameTypeRequiredDescription
typestringNoBundle type: 'inbound' or 'outbound' (default: 'inbound')

Response (JSON)

Kept packets grouped by date directory, newest first

FieldTypeDescription
successbooleanTrue on success
groupsarrayDate-grouped list of packet entries
groups[].datestringDate directory label (e.g. "Mar-18-2026"), empty for loose root-level files
groups[].packetsarrayPacket and bundle records within this date group
groups[].packets[].file_typestringEither "pkt" (raw packet) or "bundle" (arcmail archive)
groups[].packets[].filenamestringFilename within the keep directory
groups[].packets[].sizeintegerFile size in bytes
groups[].packets[].modifiedstringISO 8601 UTC last-modified timestamp
groups[].packets[].modified_tsintegerUnix timestamp of last modification
groups[].packets[].message_countintegerNumber of messages (pkt only)
groups[].packets[].dest_addressstringDestination FidoNet address (pkt only)
groups[].packets[].orig_addressstringOriginating FidoNet address (pkt only)
groups[].latest_modified_tsintegerUnix timestamp of the most recently modified file in this group
totalintegerTotal number of packet/bundle files across all groups

Error Responses

StatusDescription
400Invalid type parameter
403License not valid or user lacks BinkP admin privileges

GET /api/binkp/logs

Requires authentication

Fetches recent BinkP protocol logs. Requires BinkP admin privileges. Supports configurable line count for pagination.

Query Parameters

NameTypeRequiredDescription
linesintegerNoNumber of log lines to retrieve (default: 100)

Response (JSON)

Recent log lines from all BinkP-related log files

FieldTypeDescription
successbooleanTrue on success
logsarray of stringsLog lines in "<filename>: <raw log line>" format, up to lines entries per file, newest last

Error Responses

StatusDescription
403User lacks BinkP admin privileges

GET /api/binkp/logs/search

Requires authentication

Searches BinkP logs for entries matching a query. Requires BinkP admin privileges. Query must be at least 2 characters. Results are JSON-encoded with UTF-8 substitution for invalid sequences.

Query Parameters

NameTypeRequiredDescription
qstringYesSearch query (minimum 2 characters)

Response (JSON)

PID-contextual log search results — all lines from sessions that contain the query term

FieldTypeDescription
successbooleanTrue on success
linesarrayLog line entries (all lines from matching PIDs across all BinkP log files)
lines[].linestringFull log line prefixed with "<filename>: "
lines[].is_matchbooleanTrue if this line itself contains the query term (as opposed to being context from a matching PID)
lines[].pidstringProcess ID extracted from the log line
pid_countintegerNumber of distinct PIDs whose sessions contained the query term
match_countintegerNumber of lines that directly matched the query term

Error Responses

StatusDescription
400Query string less than 2 characters
403User lacks BinkP admin privileges
500Failed to encode search results

Bulletins

MethodPathAuthSummary
GET/api/bulletinsYesRetrieve active bulletins and unread count for user.
POST/api/bulletins/{id}/readYesMark a single bulletin as read for the authenticated user.
POST/api/bulletins/read-allYesMark multiple bulletins as read in a single request.

GET /api/bulletins

Requires authentication

Returns list of active bulletins visible to the user, unread count, and the configured bulletin display mode. Bulletins are filtered based on user permissions and read status.

Response (JSON)

Bulletins and metadata

FieldTypeDescription
successbooleanAlways true on success
bulletinsarrayArray of active bulletin objects
bulletins[].idintegerBulletin ID
bulletins[].titlestringBulletin title
bulletins[].bodystringBulletin body text (raw source)
bulletins[].formatstringBody format (markdown, html, plain)
bulletins[].sort_orderintegerDisplay sort order
bulletins[].is_activebooleanWhether bulletin is active
bulletins[].active_fromstring|nullISO 8601 start date (null = always active)
bulletins[].active_untilstring|nullISO 8601 expiry date (null = no expiry)
bulletins[].created_byintegerUser ID of bulletin creator
bulletins[].is_readbooleanWhether the authenticated user has read this bulletin
bulletins[].body_htmlstringBulletin body rendered to HTML
unread_countintegerNumber of unread bulletins for this user
bulletin_display_modestringConfigured display mode (e.g., popup, list, none)

Error Responses

StatusDescription
401Authentication required

POST /api/bulletins/{id}/read

Requires authentication

Records that the authenticated user has read a specific bulletin. Uses the bulletin ID from the URL path. Returns success confirmation. No validation of bulletin existence is performed in the snippet.

Path Parameters

NameTypeDescription
idintegerThe bulletin ID to mark as read

Response (JSON)

JSON object with success status

FieldTypeDescription
successbooleanAlways true on successful completion

POST /api/bulletins/read-all

Requires authentication

Marks a batch of bulletins as read for the authenticated user. Accepts a JSON array of bulletin IDs in the request body. Validates that the ids field is an array before processing. Returns success confirmation or a 400 error if validation fails.

Request Body (JSON)

JSON object containing array of bulletin IDs

FieldTypeRequiredDescription
idsarray of integersYesArray of bulletin IDs to mark as read

Response (JSON)

JSON object with success status

FieldTypeDescription
successbooleanTrue when all bulletins are marked as read

Error Responses

StatusDescription
400Invalid bulletin list (ids is not an array)

Chat

MethodPathAuthSummary
GET/api/chat/roomsYesList all active chat rooms.
GET/api/chat/onlineYesGet list of online users and active bots.
GET/api/chat/messagesYesFetch chat messages from a room or direct message thread.
GET/api/chat/cursorYesReturn the current maximum visible chat message ID for the user.
POST/api/chat/sendYesSend a message to a chat room or direct message.
POST/api/chat/moderateYesModerate chat: kick or ban user from room.
GET/api/chat/pollYesPoll for new chat messages since last check.

GET /api/chat/rooms

Requires authentication

Retrieves a list of all active chat rooms available on the BBS. Returns room ID, name, and description for each room. Chat feature must be enabled. Useful for populating room selection UI.

Response (JSON)

Array of active chat rooms

FieldTypeDescription
roomsarrayList of room objects
rooms[].idintegerUnique room identifier
rooms[].namestringRoom display name
rooms[].descriptionstringRoom description

Error Responses

StatusDescription
403Chat feature is disabled

GET /api/chat/online

Requires authentication

Returns users currently online (within 15 minutes) plus all active AI bots, excluding the authenticated user. Bots are always listed regardless of session state. Useful for presence indicators and direct message targeting.

Response (JSON)

Array of online users and bots

FieldTypeDescription
online_usersarrayList of online user objects
online_users[].user_idintegerUser ID
online_users[].usernamestringUser's display name
online_users[].locationstringUser's current location (may be empty)
online_users[].is_botbooleanTrue if user is an AI bot

Error Responses

StatusDescription
403Chat feature is disabled

GET /api/chat/messages

Requires authentication

Retrieves paginated messages from either a chat room or a direct message conversation. Supports cursor-based pagination via before_id. Must specify exactly one of room_id or dm_user_id. Returns up to 200 messages ordered newest first.

Query Parameters

NameTypeRequiredDescription
room_idintegerNoChat room ID (mutually exclusive with dm_user_id)
dm_user_idintegerNoUser ID for direct message thread (mutually exclusive with room_id)
before_idintegerNoFetch messages before this message ID (for pagination)
limitintegerNoMax messages to return (default 50, max 200)

Response (JSON)

Array of chat messages

FieldTypeDescription
messagesarrayList of message objects
messages[].idintegerMessage ID
messages[].room_idintegernull
messages[].room_namestringnull
messages[].from_user_idintegerSender user ID
messages[].from_usernamestringSender username
messages[].to_user_idintegernull
messages[].bodystringMessage text
messages[].created_atstringISO 8601 timestamp

Error Responses

StatusDescription
400Invalid query: must specify exactly one of room_id or dm_user_id
403Chat feature is disabled

POST /api/chat/send

Requires authentication

Posts a new message to either a room or direct message thread. Supports special commands: /source (GitHub URL), /help (command list), /kick and /ban (admin only). Message body must be 1-1000 characters. Exactly one of room_id or to_user_id must be specified.

Request Body (JSON)

Message to send

FieldTypeRequiredDescription
room_idintegerNoTarget room ID (mutually exclusive with to_user_id)
to_user_idintegerNoTarget user ID for DM (mutually exclusive with room_id)
bodystringYesMessage text (1-1000 characters)

Response (JSON)

Confirmation of sent message or local system message

FieldTypeDescription
successbooleanWhether message was sent
local_messageobjectSystem message object (for /help, /source, or errors)
local_message.from_usernamestringAlways 'System' for local messages
local_message.bodystringMessage content
local_message.typestringAlways 'local' for system messages

Error Responses

StatusDescription
400Invalid target (must specify exactly one of room_id or to_user_id)
400Message length invalid (must be 1-1000 characters)
403Admin required for /kick or /ban commands
403Chat feature is disabled

POST /api/chat/moderate

Requires authentication

Admin-only endpoint to kick (10-minute temporary ban) or permanently ban a user from a chat room. Validates room and user existence before applying action. Requires admin privileges.

Request Body (JSON)

Moderation action

FieldTypeRequiredDescription
room_idintegerYesTarget chat room ID
user_idintegerYesUser ID to kick or ban
actionstringYesEither 'kick' (10 min) or 'ban' (permanent)

Response (JSON)

Confirmation of moderation action

FieldTypeDescription
successbooleanWhether action was applied

Error Responses

StatusDescription
400Invalid moderation request (missing fields or invalid action)
403Admin privileges required
404Chat room not found
404User not found or inactive
403Chat feature is disabled

GET /api/chat/cursor

Requires authentication

Returns the highest chat message ID currently visible to the authenticated user across active rooms and direct messages addressed to them. This is useful for clients that want to anchor a polling cursor at "now" without replaying older backlog from other rooms or DM threads.

Response (JSON)

Current visible chat cursor

FieldTypeDescription
max_idintegerHighest visible chat message ID for the authenticated user

Error Responses

StatusDescription
400Invalid chat user context
403Chat feature is disabled

GET /api/chat/poll

Requires authentication

Long-polling endpoint that returns new messages (room and DM) since the provided since_id cursor. Excludes messages from the authenticated user. Returns up to 200 messages with HTML markup rendered. Useful for clients that don't support Server-Sent Events.

Query Parameters

NameTypeRequiredDescription
since_idintegerNoReturn messages with ID greater than this (default 0)

Response (JSON)

Array of new messages since cursor

FieldTypeDescription
messagesarrayList of message objects
messages[].idintegerMessage ID
messages[].typestringEither 'room' or 'dm'
messages[].room_idintegernull
messages[].room_namestringnull
messages[].from_user_idintegerSender user ID
messages[].from_usernamestringSender username
messages[].to_user_idintegernull
messages[].bodystringRaw message text
messages[].markup_htmlstringHTML-rendered message (markdown processed)
messages[].created_atstringISO 8601 timestamp

Error Responses

StatusDescription
403Chat feature is disabled

Credits

MethodPathAuthSummary
POST/api/credits/sendYesSend credits from authenticated user to another user.

POST /api/credits/send

Requires authentication

Transfers credits between users with validation of amount, recipient existence, and sender balance. Credits feature must be enabled. Amount must be between 1 and 200. Users cannot send credits to themselves. Creates transaction records for both parties.

Request Body (JSON)

Credit transfer request

FieldTypeRequiredDescription
recipient_idintegerYesID of the user receiving credits
amountintegerYesNumber of credits to send (1-200)
messagestringNoOptional message to include with transfer

Response (JSON)

Transfer confirmation with updated balances

FieldTypeDescription
successbooleanTransfer success flag
sender_balanceintegerSender's new credit balance
recipient_balanceintegerRecipient's new credit balance

Error Responses

StatusDescription
400Credits feature disabled, invalid amount, self-transfer, or insufficient balance
404Recipient not found or inactive

Dashboard

MethodPathAuthSummary
GET/api/dashboard/statsYesRetrieve dashboard statistics for authenticated user.
POST/api/dashboard/layoutYesSave or reset user's dashboard card layout.

GET /api/dashboard/stats

Requires authentication

Returns aggregated dashboard statistics including user activity, message counts, and system metrics. Statistics are computed by DashboardStatsService and may vary based on user role and permissions.

Response (JSON)

Dashboard statistics object. All counts are for the authenticated user.

FieldTypeDescription
unread_netmailintegerNetmail badge count (non-zero only when new messages arrived since last check)
total_netmailintegerTrue unread netmail count
new_echomailintegerEchomail messages in subscribed areas since last visit
online_countintegerUsers active in the last 15 minutes
unread_bulletinsintegerUnread bulletins for the authenticated user
credit_balanceintegerUser credit balance (0 if credits disabled)
chat_totalintegerNew chat messages since last visit
new_filesintegerNew approved files since last visit
new_echoareasintegerEcho areas created in the last 30 days
recent_echoareasarrayUp to 8 most recently created echo area objects
recent_echoareas[].idintegerEcho area ID
recent_echoareas[].tagstringEcho area tag name
recent_echoareas[].domainstring|nullNetwork domain
recent_echoareas[].descriptionstring|nullEcho area description
recent_echoareas[].created_atstringISO 8601 creation timestamp
echomail_max_idintegerCurrent max echomail row ID (used for badge tracking)
chat_max_idintegerCurrent max chat message ID
files_max_idintegerCurrent max file ID
total_filesintegerTotal approved files
pending_file_approvalsinteger(admin only) Files pending approval
pending_echomail_moderationinteger(admin only) Echomail messages pending moderation

Error Responses

StatusDescription
401Authentication required

POST /api/dashboard/layout

Requires authentication

Persists custom dashboard layout configuration or resets to defaults. Validates layout against available cards (which may depend on user role and feature flags like referral credits). Supports reset flag to clear saved layout.

Request Body (JSON)

Dashboard layout configuration

FieldTypeRequiredDescription
resetbooleanNoIf true, clears saved layout and uses defaults on next load
cardsarrayNoArray of card configurations (structure validated against available cards)

Response (JSON)

Layout save confirmation

FieldTypeDescription
successbooleanWhether layout was saved or reset

Error Responses

StatusDescription
400Invalid layout data or validation failed

Debug

MethodPathAuthSummary
GET/api/admin/debugYesDebug endpoint for authentication testing

GET /api/admin/debug

Requires authentication

Returns current authenticated user info, admin status, and session cookie details. Useful for debugging auth issues and verifying session state.

Response (JSON)

Current authentication state

FieldTypeDescription
userobjectCurrent user object (null if not authenticated)
user.user_idintegerUser ID
user.usernamestringUsername
user.real_namestringReal name
user.emailstring|nullEmail address
user.is_adminbooleanAdmin flag
is_adminbooleanWhether current user has admin privileges
cookie_presentbooleanWhether session cookie exists
cookie_valuestring|nullSession cookie value (null if not present)

Error Responses

StatusDescription
500Auth check failed

Docs

MethodPathAuthSummary
GET/api/docs/mcp-client-help/claudeYesRetrieve MCP client help documentation as HTML.

GET /api/docs/mcp-client-help/claude

Requires authentication

Returns rendered HTML version of MCPClientHelp.md markdown documentation. Requires authentication and valid license. Useful for embedding help content in client applications.

Response (JSON)

Rendered help documentation in HTML format.

FieldTypeDescription
successbooleanOperation success flag
htmlstringHTML-rendered markdown content

Error Responses

StatusDescription
404Help file not found

Echoareas

MethodPathAuthSummary
GET/api/echoareasYesList echo areas with filtering, subscription, and message counts.
POST/api/echoareas/mark-readYesMark all unread messages in one or more echo areas as read in bulk.
GET/api/echoareas/{id}YesGet detailed echo area configuration with LovlyNet metadata.
POST/api/echoareasYesCreate a new echo area with configuration.
PUT/api/echoareas/{id}YesUpdate echo area configuration.
DELETE/api/echoareas/{id}YesDelete an echo area.
GET/api/echoareas/statsYesGet echo area statistics.
GET/api/echoareas/simple-listYesLightweight list of all echo areas for admin comboboxes.

GET /api/echoareas

Requires authentication

Retrieves a paginated list of echo areas with support for filtering by status (active/inactive/all), subscription status, and visibility rules. Returns message counts (total and unread), subscriber counts, and last post metadata. Respects user permissions and moderation filters. Admins see all areas; regular users see only non-sysop areas they're subscribed to or have access to.

Query Parameters

NameTypeRequiredDescription
filterstringNoFilter by status: 'active' (default), 'inactive', or 'all'
subscribed_onlybooleanNoIf 'true', return only areas the user is subscribed to (default: false)

Response (JSON)

Array of echo area objects with message and subscription metadata

FieldTypeDescription
idintegerEcho area ID
tagstringEcho area tag (uppercase)
descriptionstringHuman-readable description
moderatorstringnull
message_countintegerTotal visible messages (respects user permissions)
unread_countintegerUnread messages for current user
subscriber_countintegerNumber of active subscribers
last_subjectstringnull
last_authorstringnull
last_datestringnull
is_activebooleanWhether area is active
is_sysop_onlybooleanWhether area is restricted to sysops
allow_mediabooleannull
colorstringHex color code for UI display

Error Responses

StatusDescription
401Authentication required

POST /api/echoareas/mark-read

Requires authentication

Marks all currently unread, visible messages in one or more echo areas as read for the authenticated user and advances the last_read_id watermark per echoarea. Applies the same ignore-rule and moderation visibility filters used by GET /api/echoareas, and skips sysop-only areas for non-admin users. Uses a database transaction for consistency.

Request Body (JSON)

List of echo area IDs to mark as read

FieldTypeRequiredDescription
echoareaIdsarrayYesNon-empty array of echo area IDs

Response (JSON)

Read status update summary

FieldTypeDescription
successbooleanOperation succeeded
markedintegerNumber of messages marked as read
areasintegerNumber of distinct echo area IDs processed

Error Responses

StatusDescription
400echoareaIds missing, empty, or not an array

GET /api/echoareas/{id}

Requires authentication

Retrieves full configuration for a single echo area including all settings and optional LovlyNet integration metadata. Admin-only endpoint. If the area is configured for LovlyNet domain, fetches remote metadata and validates local settings against recommended values, reporting any mismatches.

Path Parameters

NameTypeDescription
idintegerEcho area ID

Response (JSON)

Single echo area object with extended metadata

FieldTypeDescription
echoareaobjectFull echo area record including all database columns
echoarea.idintegerEcho area ID
echoarea.tagstringEcho area tag
echoarea.descriptionstringDescription
echoarea.domainstringDomain (e.g., 'lovlynet')
echoarea.missing_chrs_charsetstring|nullFallback charset used when inbound FTN messages for this area have no CHRS kludge
echoarea.is_sysop_onlybooleanSysop-only flag
echoarea.is_activebooleanWhether area is active
echoarea.is_localbooleanWhether area is local-only (not forwarded)
echoarea.colorstringHex color code for UI display
echoarea.lovlynet_metadataobjectRemote LovlyNet metadata if domain is 'lovlynet'; empty object otherwise
echoarea.lovlynet_metadata.sysop_onlybooleanLovlyNet recommended sysop-only setting
echoarea.lovlynet_setting_issuesarrayArray of setting mismatches with recommended vs actual values
echoarea.lovlynet_setting_issues[].settingstringSetting name that has a mismatch
echoarea.lovlynet_setting_issues[].recommendedbooleanLovlyNet recommended value
echoarea.lovlynet_setting_issues[].actualbooleanCurrent local value
echoarea.lovlynet_has_setting_issuesbooleanWhether any setting mismatches exist
echoarea.description_mismatchbooleanWhether local description differs from LovlyNet description

Error Responses

StatusDescription
401Authentication required
403Admin privileges required
404Echo area not found

POST /api/echoareas

Requires authentication

Creates a new echo area with full configuration including posting name policy, art format hints, and media settings. Admin-only. Validates tag format (uppercase alphanumeric with dots, underscores, hyphens, apostrophes). Supports inheritance of policies from system defaults via null values.

Request Body (JSON)

Echo area configuration

FieldTypeRequiredDescription
tagstringYesUppercase tag matching /^[A-Z0-9._'-]+$/
descriptionstringYesHuman-readable description
moderatorstringnullNo
uplink_addressstringnullNo
colorstringNoHex color code (default: #28a745)
is_activebooleanNoWhether area is active
is_localbooleanNoWhether area is local-only
is_sysop_onlybooleanNoWhether area is sysop-only
domainstringNoDomain name (e.g., 'lovlynet')
posting_name_policystringnullNo
art_format_hintstringnullNo
missing_chrs_charsetstringnullNo
allow_mediastringNo'allow'/'true', 'deny'/'false', or 'inherit' (default)
gemini_publicbooleanNoWhether area is public on Gemini protocol

Response (JSON)

Created echo area with ID

FieldTypeDescription
successbooleantrue on success
idintegerNew echo area ID
message_codestringLocalization key for success message

Error Responses

StatusDescription
400Validation error (invalid tag format, missing required fields, duplicate tag)
403Admin privileges required

PUT /api/echoareas/{id}

Requires authentication

Updates an existing echo area's configuration. Admin-only. Validates all fields same as POST. Supports partial updates; omitted fields retain current values. Tag must be unique unless unchanged.

Path Parameters

NameTypeDescription
idintegerEcho area ID

Request Body (JSON)

Echo area configuration (same as POST, all fields optional)

FieldTypeRequiredDescription
tagstringNoUppercase tag
descriptionstringNoDescription
moderatorstringnullNo
uplink_addressstringnullNo
colorstringNoHex color code
is_activebooleanNoActive status
is_localbooleanNoLocal-only flag
is_sysop_onlybooleanNoSysop-only flag
domainstringNoDomain name
posting_name_policystringnullNo
art_format_hintstringnullNo
missing_chrs_charsetstringnullNo
allow_mediastringNoMedia policy
gemini_publicbooleanNoGemini public flag

Response (JSON)

Updated echo area

FieldTypeDescription
successbooleantrue on success
message_codestringLocalization key

Error Responses

StatusDescription
400Validation error or echo area not found
403Admin privileges required

DELETE /api/echoareas/{id}

Requires authentication

Deletes an echo area only if it contains no messages. Admin-only. Returns error if area has messages; deactivation is recommended instead. Cascades delete to subscriptions and related data.

Path Parameters

NameTypeDescription
idintegerEcho area ID

Response (JSON)

Deletion confirmation

FieldTypeDescription
successbooleantrue on success
message_codestringLocalization key for success message

Error Responses

StatusDescription
400Cannot delete area with messages, or area not found
403Admin privileges required

GET /api/echoareas/stats

Requires authentication

Returns aggregate statistics for all echo areas: count of active areas, total messages across all areas, and messages posted today. Useful for dashboard/monitoring.

Response (JSON)

Echo area statistics

FieldTypeDescription
active_countintegerNumber of active echo areas
total_messagesintegerTotal messages across all areas
today_messagesintegerMessages posted today (UTC)

Error Responses

StatusDescription
401Authentication required

GET /api/echoareas/simple-list

Requires authentication

Returns a minimal echo area listing suitable for populating admin UI dropdowns. Includes only essential fields: id, tag, description, and domain. Sorted alphabetically by tag.

Response (JSON)

Array of echo areas

FieldTypeDescription
echoareasarrayArray of minimal echo area objects
echoareas[].idintegerEcho area ID
echoareas[].tagstringEcho area tag
echoareas[].descriptionstringHuman-readable description
echoareas[].domainstringDomain name (e.g., fidonet, lovlynet)

Fileareas

MethodPathAuthSummary
GET/api/fileareasYesList file areas with LovlyNet metadata.
GET/api/fileareas/{id}YesGet detailed file area configuration.
POST/api/fileareasYesCreate a new file area.
PUT/api/fileareas/{id}YesUpdate an existing file area.
DELETE/api/fileareas/{id}YesDelete a file area.
GET/api/fileareas/statsYesGet file area statistics.
GET/api/fileareas/{id}/preview-isoYesPreview ISO file import without committing changes.
POST/api/fileareas/{id}/reindex-isoYesRe-index an ISO file area with optional overrides.
DELETE/api/fileareas/{id}/subfolderYesDelete all files in a subfolder.
POST/api/fileareas/{id}/comment-areaYesAdmin: link, create, or unlink a comment echo area for a file area.

GET /api/fileareas

Requires authentication

Retrieves file areas with filtering by status and user access level. Returns ISO mount point accessibility status. Fetches LovlyNet metadata for areas in the 'lovlynet' domain. Respects admin/user/public visibility rules.

Query Parameters

NameTypeRequiredDescription
filterstringNoFilter by status: 'active' (default), 'inactive', or 'all'

Response (JSON)

Array of file area objects with metadata

FieldTypeDescription
idintegerFile area ID
tagstringFile area tag
descriptionstringDescription
area_typestringType: 'iso', 'local', etc.
iso_accessiblebooleanWhether ISO mount point is readable (if area_type='iso')
domainstringDomain name
is_activebooleanWhether area is active

Error Responses

StatusDescription
401Authentication required

GET /api/fileareas/{id}

Requires authentication

Retrieves full configuration for a single file area including ISO mount point accessibility. Admin-only endpoint.

Path Parameters

NameTypeDescription
idintegerFile area ID

Response (JSON)

Single file area object with full configuration

FieldTypeDescription
fileareaobjectFull file area configuration (all database columns)
filearea.idintegerFile area ID
filearea.tagstringFile area tag
filearea.descriptionstringDescription
filearea.domainstringDomain name (e.g. fidonet)
filearea.is_activebooleanWhether area is active
filearea.is_localbooleanWhether area is local-only
filearea.is_privatebooleanWhether area is private
filearea.is_publicbooleanWhether area is publicly accessible without login
filearea.area_typestringArea type: normal or iso
filearea.iso_mount_pointstringnull
filearea.iso_accessiblebooleanWhether ISO mount point is currently readable
filearea.comment_echoarea_idintegernull
filearea.upload_permissionintegerUpload permission level (1 = users, 2 = admin only)
filearea.file_countintegerNumber of approved files in area
filearea.total_sizeintegerTotal size of all files in bytes
filearea.created_atstringISO 8601 creation timestamp
filearea.updated_atstringISO 8601 last update timestamp

Error Responses

StatusDescription
403Admin privileges required
404File area not found

POST /api/fileareas

Requires authentication

Creates a new file area with the provided configuration. Requires admin authentication. Returns the newly created file area ID on success. The request body should contain file area configuration details passed to FileAreaManager::createFileArea().

Request Body (JSON)

File area configuration object

Response (JSON)

Success response with created file area ID

FieldTypeDescription
successbooleanAlways true on success
idintegerID of the newly created file area
message_codestringLocalization key for success message

Error Responses

StatusDescription
400Failed to create file area (invalid data or database error)

PUT /api/fileareas/{id}

Requires authentication

Updates a file area identified by ID with new configuration data. Requires admin authentication. Modifies the file area in-place and returns success status without the updated object.

Path Parameters

NameTypeDescription
idintegerFile area ID to update

Request Body (JSON)

File area configuration updates

Response (JSON)

Success response

FieldTypeDescription
successbooleanAlways true on success
message_codestringLocalization key for success message

Error Responses

StatusDescription
400Failed to update file area (invalid data or database error)

DELETE /api/fileareas/{id}

Requires authentication

Permanently deletes a file area and all associated data. Requires admin authentication. This operation cannot be undone.

Path Parameters

NameTypeDescription
idintegerFile area ID to delete

Response (JSON)

Success response

FieldTypeDescription
successbooleanAlways true on success
message_codestringLocalization key for success message

Error Responses

StatusDescription
400Failed to delete file area (database error)

GET /api/fileareas/stats

Requires authentication

Retrieves aggregated statistics for all file areas. Requires authentication. Returns different data based on user privilege level (guest vs. authenticated users).

Response (JSON)

File area statistics

FieldTypeDescription
active_countintegerNumber of active file areas (public-only when request is unauthenticated)
total_filesintegerTotal approved file count across matching areas
total_sizeintegerTotal byte size of all files across matching areas

GET /api/fileareas/{id}/preview-iso

Requires authentication

Performs a dry-run scan of an ISO file area, returning directory entries with descriptions and import status. Requires admin authentication. Supports flat listing and catalogue-only modes via query parameters. Does not modify the database.

Path Parameters

NameTypeDescription
idintegerFile area ID to preview

Query Parameters

NameTypeRequiredDescription
flatbooleanNoIf set, return flat file list instead of hierarchical structure
catalogue_onlybooleanNoIf set, only include catalogued entries

Response (JSON)

Preview data with success flag and directory entries

FieldTypeDescription
successbooleanAlways true on success

Error Responses

StatusDescription
500ISO preview failed (file read or parsing error)

POST /api/fileareas/{id}/reindex-iso

Requires authentication

Triggers a re-index of an ISO file area, importing or updating file entries. Requires admin authentication. Supports per-file overrides for descriptions and skip flags. Returns import counters (added, updated, skipped, etc.).

Path Parameters

NameTypeDescription
idintegerFile area ID to re-index

Request Body (JSON)

ISO import configuration

FieldTypeRequiredDescription
flatbooleanNoUse flat import structure
catalogue_onlybooleanNoOnly import catalogued files
overridesarrayNoArray of per-file overrides with rel_path, description, and skip flag

Response (JSON)

Import result with counters

FieldTypeDescription
successbooleanAlways true on success
countersobjectImport statistics
counters.importedintegerNumber of new files added
counters.updatedintegerNumber of existing files updated
counters.skippedintegerNumber of files skipped (unchanged or already present)
counters.no_descriptionintegerNumber of files with no description available
counters.errorsintegerNumber of files that failed to import

Error Responses

StatusDescription
500ISO re-index failed (file processing or database error)

DELETE /api/fileareas/{id}/subfolder

Requires authentication

Removes all files and iso_subdir records belonging to a specified subfolder path, including nested subfolders. Requires admin authentication. Returns count of deleted files.

Path Parameters

NameTypeDescription
idintegerFile area ID

Request Body (JSON)

Subfolder deletion request

FieldTypeRequiredDescription
subfolderstringYesSubfolder path to delete (cannot be empty)

Response (JSON)

Deletion result

FieldTypeDescription
successbooleanAlways true on success
deletedintegerNumber of files deleted

Error Responses

StatusDescription
400Subfolder parameter is required or empty
500Failed to delete subfolder (database error)

POST /api/fileareas/{id}/comment-area

Requires authentication

Manages the comment echo area association for a file area. Supports three actions: 'link' to attach an existing echo area, 'create' to generate a new echo area, or 'unlink' to remove the association. Tag validation enforces FidoNet naming conventions. Admin-only endpoint.

Path Parameters

NameTypeDescription
idintegerFile area ID

Request Body (JSON)

Comment area action configuration

FieldTypeRequiredDescription
actionstringYesOne of: 'link', 'create', 'unlink'
echoarea_idintegerNoEcho area ID (required for 'link' action)
tagstringNoEcho area tag in uppercase (required for 'create' action, must match /^[A-Z0-9._'-]+$/)
descriptionstringNoEcho area description (optional for 'create' action)

Response (JSON)

Updated file area with comment area configuration

FieldTypeDescription
successbooleanOperation success status
comment_echoarea_idintegernull

Error Responses

StatusDescription
400Missing required field (echoarea_id for link, tag for create) or invalid tag format
404File area or echo area not found

Files

MethodPathAuthSummary
GET/api/filesYesList files in a file area with optional subfolder filtering.
GET/api/files/recentYesRetrieve recently uploaded files across all accessible areas.
GET/api/files/my-uploadsYesList all files uploaded by the authenticated user.
GET/api/files/searchYesSearch files by name and description across accessible areas.
GET/api/files/{id}YesRetrieve detailed metadata for a specific file.
POST/api/files/{id}/rehatchYesRe-hatch a file via the admin daemon (admin only).
GET/api/files/{id}/downloadYesDownload a file with access control and credit deduction.
GET/api/files/{id}/previewYesPreview a file inline (images, video, audio, text).
GET/api/files/{id}/prgsYesExtract and return PRG files from archives as base64-encoded JSON.
GET/api/files/{id}/zip-contentsYesList non-directory entries inside a .zip file.
GET/api/files/{id}/zip-entryYesServe a single entry from a .zip file for inline preview.
GET/api/files/{id}/archive-contentsYesList entries in any supported archive format.
GET/api/files/{id}/archive-entryYesServe a single entry from any supported archive.
POST/api/files/{id}/shareYesCreate a share link for a file.
GET/api/files/shared/check/{fileId}YesCheck if user has an active share for a file.
GET/api/files/shared/{area}/{filename}YesGet shared file info by area tag and filename.
DELETE/api/files/shares/{shareId}YesRevoke a file share link.
POST/api/files/uploadYesUpload a file to a file area with descriptions and optional cost deduction.
POST/api/files/add-linkYesAdd an external URL link to a file area as a file entry.
POST/api/files/fetch-url-metaYesFetch page title and metadata from a URL for link preview.
DELETE/api/files/{id}/deleteYesDelete a file from a file area (owner or admin).
PUT/api/files/{id}/renameYesEdit file name and/or descriptions (owner or admin).
POST/api/files/{id}/scanYesTrigger on-demand ClamAV virus scan for a file (admin only).
PUT/api/files/{id}/scan-statusYesManually override virus scan status for a file (admin only).
GET/api/files/{id}/commentsYesFetch threaded echomail comments linked to a file.
POST/api/files/{id}/commentsYesPost a comment on a file, creating a thread root if needed.

GET /api/files

Requires authentication

Retrieves files and subfolders from a specified file area. Supports public areas (guest access) and private areas (authenticated users only). Requires area_id query parameter. Optional subfolder parameter filters results; empty string or missing parameter returns root level. Returns subfolders, files, and breadcrumb navigation.

Query Parameters

NameTypeRequiredDescription
area_idintegerYesFile area ID to list
subfolderstringNoSubfolder path to list (omit or empty for root)

Response (JSON)

Files and subfolders in the area

FieldTypeDescription
subfoldersarrayList of subdirectory objects
subfolders[].subfolderstringSubfolder path
subfolders[].descriptionstring|nullDisplay label from ISO metadata (if present)
subfolders[].long_descriptionstring|nullExtended ISO subfolder description
subfolders[].subdir_idinteger|nullID of the iso_subdir record if applicable
filesarrayList of file objects in current folder
files[].idintegerFile ID
files[].filenamestringFile name
files[].filesizeintegerFile size in bytes
files[].short_descriptionstringBrief description
files[].long_descriptionstringExtended description
files[].statusstringApproval status (approved, pending, rejected)
files[].source_typestringOrigin type (fidonet, user_upload, iso_import, url, etc.)
files[].created_atstringUpload timestamp (ISO 8601)
files[].subfolderstring|nullSubfolder path if in a subdirectory
files[].owner_idinteger|nullUser ID of uploader
files[].area_tagstringTag of the file area
files[].is_sharedbooleanWhether an active share link exists for this file
subfolderstring|nullCurrent subfolder path (null at root level)
subfolder_labelstring|nullDisplay label for current subfolder (null at root level)

Error Responses

StatusDescription
404File areas feature is disabled
400File area ID is required
403User does not have access to this file area

GET /api/files/recent

Requires authentication

Returns a paginated list of the most recently uploaded files visible to the authenticated user. Guests see only public files. The limit parameter is capped at 50 to prevent abuse. File areas must be enabled.

Query Parameters

NameTypeRequiredDescription
limitintegerNoMaximum number of files to return (default 25, max 50)

Response (JSON)

Array of recent file objects

FieldTypeDescription
filesarrayList of file objects
files[].idintegerFile ID
files[].filenamestringFile name
files[].filesizeintegerFile size in bytes
files[].short_descriptionstringBrief description
files[].created_atstringUpload timestamp (ISO 8601)
files[].subfolderstring|nullSubfolder path if applicable
files[].subfolder_labelstring|nullDisplay label for subfolder from ISO metadata
files[].source_typestringOrigin type (fidonet, user_upload, iso_import, url, etc.)
files[].area_tagstringTag of the file area
files[].domainstringDomain of the file area
files[].is_localbooleanWhether the file area is local-only
files[].is_sharedbooleanWhether an active share link exists for this file

Error Responses

StatusDescription
404File areas feature is disabled

GET /api/files/my-uploads

Requires authentication

Returns the authenticated user's uploaded files along with a summary of their upload statistics. Requires authentication. File areas must be enabled.

Response (JSON)

User's uploads and summary statistics

FieldTypeDescription
filesarrayList of file objects uploaded by the user
files[].idintegerFile ID
files[].filenamestringFile name
files[].filesizeintegerFile size in bytes
files[].short_descriptionstringBrief description
files[].statusstringApproval status (approved, pending, rejected)
files[].created_atstringUpload timestamp (ISO 8601)
files[].area_tagstringTag of the file area
files[].domainstringDomain of the file area
files[].area_descriptionstringDescription of the file area
summaryobjectUpload statistics
summary.total_countintegerTotal number of uploads
summary.total_sizeintegerTotal size in bytes of all uploads
summary.pending_countintegerNumber of uploads awaiting approval
summary.approved_countintegerNumber of approved uploads
summary.rejected_countintegerNumber of rejected uploads

Error Responses

StatusDescription
404File areas feature is disabled

GET /api/files/search

Requires authentication

Full-text search across filenames and short descriptions in approved files. Query must be at least 2 characters. Returns up to 100 results ordered by area tag and filename. Respects area access controls: guests see only public areas, users see public + their private areas, admins see all active non-private areas.

Query Parameters

NameTypeRequiredDescription
qstringYesSearch query (minimum 2 characters)

Response (JSON)

Search results with file metadata

FieldTypeDescription
resultsarrayArray of matching file objects
results[].idintegerFile ID
results[].filenamestringFile name
results[].short_descriptionstringBrief description
results[].filesizeintegerFile size in bytes
results[].created_atstringUpload timestamp (ISO 8601)
results[].area_idintegerFile area ID
results[].area_tagstringFile area tag
results[].subfolderstring|nullSubfolder path if applicable

Error Responses

StatusDescription
404File areas feature is disabled

GET /api/files/{id}

Requires authentication

Returns full file details including metadata, area info, and access status. Guests can access files in public areas only. Authenticated users see approved files and their own pending/rejected uploads. Admins see all files. File must be approved or belong to the requesting user.

Path Parameters

NameTypeDescription
idintegerFile ID

Response (JSON)

Complete file metadata object

FieldTypeDescription
fileobjectFile details object
file.idintegerFile ID
file.filenamestringFile name
file.filesizeintegerFile size in bytes
file.short_descriptionstringBrief description
file.long_descriptionstringExtended description
file.statusstringApproval status (approved, pending, rejected)
file.source_typestringOrigin type (fidonet, user_upload, iso_import, url, etc.)
file.created_atstringUpload timestamp (ISO 8601)
file.updated_atstringLast update timestamp (ISO 8601)
file.subfolderstring|nullSubfolder path if in a subdirectory
file.urlstring|nullExternal URL (for url-type files)
file.owner_idinteger|nullUser ID of uploader
file.file_area_idintegerAssociated file area ID
file.virus_scannedbooleanWhether virus scan was performed
file.virus_scan_resultstring|nullScan result (clean, infected, error, skipped)

Error Responses

StatusDescription
404File not found or not accessible
404File areas feature is disabled

POST /api/files/{id}/rehatch

Requires authentication

Triggers file_hatch.php to re-process a file's metadata and hatch information. Admin-only operation. Cannot rehatch files in local-only or private areas. Communicates with the admin daemon to perform the operation.

Path Parameters

NameTypeDescription
idintegerFile ID to rehatch

Response (JSON)

Rehatch operation result

FieldTypeDescription
successbooleanWhether rehatch completed successfully
resultobjectCommand execution result from admin daemon
result.exit_codeintegerExit code from file_hatch.php (0 on success)
result.stdoutstringStandard output from file_hatch.php
result.stderrstringStandard error output from file_hatch.php

Error Responses

StatusDescription
403Not an admin
404File not found
400Cannot rehatch file in local-only area
400Cannot rehatch file in private area
500Rehatch operation failed

GET /api/files/{id}/download

Requires authentication

Serves a file for download with proper access control. Guests can download from public areas. Authenticated users can download approved files and their own unapprovedUploads. Admins bypass most restrictions. Senders of netmail attachments can always download their attachments. Download credits are deducted if configured.

Path Parameters

NameTypeDescription
idintegerFile ID to download

Response (JSON)

Binary file content with appropriate headers

FieldTypeDescription
Content-TypestringMIME type of the file
Content-DispositionstringAttachment header with filename

Error Responses

StatusDescription
404File not found or not accessible
404File areas feature is disabled
403Insufficient download credits

GET /api/files/{id}/preview

Requires authentication

Serves a file for in-browser preview without charging download credits. Supports images, video, audio, and text files. Unknown types are served as attachments. Allows unauthenticated access via valid file shares or public areas. No credit deduction occurs.

Path Parameters

NameTypeDescription
idintegerFile ID to preview

Query Parameters

NameTypeRequiredDescription
share_areastringNoFile area tag for shared file access
share_filenamestringNoFilename for shared file access

Response (JSON)

File content with inline Content-Disposition header

FieldTypeDescription
Content-TypestringMIME type (image/, video/, audio/, text/, etc.)
Content-DispositionstringInline header for browser preview

Error Responses

StatusDescription
404File not found or not approved
404File areas feature is disabled
403Access denied to file area

GET /api/files/{id}/prgs

Requires authentication

Extracts all PRG files from .prg, .zip, or .d64 archives and returns them as base64-encoded data with load addresses. Used by the file preview modal to render PETSCII art. The 2-byte PRG load address header is stripped before encoding. Allows unauthenticated access via valid file shares.

Path Parameters

NameTypeDescription
idintegerFile ID (must be .prg, .zip, or .d64)

Query Parameters

NameTypeRequiredDescription
share_areastringNoFile area tag for shared file access
share_filenamestringNoFilename for shared file access

Response (JSON)

Extracted PRG files with metadata

FieldTypeDescription
prgsarrayArray of PRG file objects
prgs[].namestringPRG file name
prgs[].load_addressintegerC64 load address (decimal)
prgs[].data_b64stringBase64-encoded PRG content (load address header stripped)
disk_namestringDisk name (only present for .d64 files)

Error Responses

StatusDescription
404File not found or not approved
404File areas feature is disabled
403Access denied to file area

GET /api/files/{id}/zip-contents

Requires authentication

Retrieves a list of all non-directory entries contained within a ZIP archive. Accessible to authenticated users, file owners via share links, or guests accessing public file areas. Returns entry metadata including path, name, and size. Requires the file to be approved and the file areas feature to be enabled.

Path Parameters

NameTypeDescription
idintegerFile ID of the ZIP archive

Query Parameters

NameTypeRequiredDescription
share_areastringNoFile area tag for share link access
share_filenamestringNoFilename for share link access

Response (JSON)

JSON object containing array of ZIP entries

FieldTypeDescription
entriesarrayArray of ZIP entry objects
entries[].pathstringFull path within the archive
entries[].namestringFile name (basename)
entries[].sizeintegerUncompressed file size in bytes
entries[].comp_methodstringCompression method (e.g., deflate, store)
totalintegerTotal number of entries in the archive

Error Responses

StatusDescription
401Authentication required and no valid auth/share/public access provided
404File not found, not approved, or feature disabled

GET /api/files/{id}/zip-entry

Requires authentication

Extracts and serves a single file entry from within a ZIP archive. Applies content-type detection and encoding logic for known file types to enable inline preview; unknown types are served as attachments for download. Accessible via authentication, share links, or public file areas. The file must be approved.

Path Parameters

NameTypeDescription
idintegerFile ID of the ZIP archive

Query Parameters

NameTypeRequiredDescription
pathstringYesPath to the entry within the ZIP (e.g., 'subdir/file.txt')
share_areastringNoFile area tag for share link access
share_filenamestringNoFilename for share link access

Response (JSON)

Raw file content with appropriate Content-Type header

FieldTypeDescription
bodybinaryFile entry content

Error Responses

StatusDescription
401Authentication required and no valid auth/share/public access provided
404File not found, not approved, feature disabled, or entry not found in archive

GET /api/files/{id}/archive-contents

Requires authentication

Retrieves a list of all entries from an archive file (ZIP, TAR, RAR, 7Z, etc.), auto-detected by magic bytes. Returns archive type, human-readable label, entry list, and total count. Accessible to authenticated users, share link holders, or guests on public file areas. File must be approved.

Path Parameters

NameTypeDescription
idintegerFile ID of the archive

Query Parameters

NameTypeRequiredDescription
share_areastringNoFile area tag for share link access
share_filenamestringNoFilename for share link access

Response (JSON)

JSON object with archive metadata and entry list

FieldTypeDescription
typestringArchive format code (e.g., 'zip', 'tar', 'rar')
labelstringHuman-readable archive type label
entriesarrayArray of archive entry objects
entries[].pathstringFull path within the archive
entries[].namestringFile name (basename)
entries[].sizeintegerUncompressed file size in bytes
entries[].comp_methodstringCompression method (present for ZIP entries only)
totalintegerTotal number of entries in the archive

Error Responses

StatusDescription
401Authentication required and no valid auth/share/public access provided
404File not found, not approved, feature disabled, or unsupported archive format

GET /api/files/{id}/archive-entry

Requires authentication

Extracts and serves a single file entry from any supported archive format (auto-detected by magic bytes). Applies content-type detection for inline preview or download based on file type. Accessible via authentication, share links, or public file areas.

Path Parameters

NameTypeDescription
idintegerFile ID of the archive

Query Parameters

NameTypeRequiredDescription
pathstringYesPath to the entry within the archive
share_areastringNoFile area tag for share link access
share_filenamestringNoFilename for share link access

Response (JSON)

Raw file content with appropriate Content-Type header

FieldTypeDescription
bodybinaryFile entry content

Error Responses

StatusDescription
401Authentication required and no valid auth/share/public access provided
404File not found, not approved, feature disabled, or entry not found in archive

POST /api/files/{id}/share

Requires authentication

Generates a shareable link for a file, allowing unauthenticated access. If a share already exists, returns the existing share. Supports optional expiration in hours and frequency-accessible flag. Returns share metadata including ID and access tracking info.

Path Parameters

NameTypeDescription
idintegerFile ID to share

Request Body (JSON)

Share creation parameters

FieldTypeRequiredDescription
expires_hoursintegerNoHours until share expires; null for no expiration
freq_accessiblebooleanNoWhether share is frequently accessible (default: true)

Response (JSON)

Share creation result with share details

FieldTypeDescription
successbooleanOperation success status
share_idintegerID of the created or existing share

Error Responses

StatusDescription
400Invalid file ID, file not found, or access denied
404Feature disabled

GET /api/files/shared/check/{fileId}

Requires authentication

Verifies whether the authenticated user has an active share link for a specific file. Returns the share URL in area/filename format, access count, last access timestamp, and revocation permission. Useful for UI to show existing shares.

Path Parameters

NameTypeDescription
fileIdintegerFile ID to check for shares

Response (JSON)

Share existence and details

FieldTypeDescription
successbooleanQuery success status
existsbooleanWhether a share exists (if false, other fields omitted)
share_idintegerShare ID (if exists)
share_urlstringFull share URL (if exists)
access_countintegerNumber of times share has been accessed
last_accessed_atstringISO timestamp of last access
can_revokebooleanWhether user can revoke this share

Error Responses

StatusDescription
404Feature disabled

GET /api/files/shared/{area}/{filename}

Requires authentication

Retrieves metadata for a shared file using its file area tag and filename. No authentication required for accessing shared files. Returns file details if the share is active and valid. Used by share link endpoints to serve files.

Path Parameters

NameTypeDescription
areastringFile area tag (URL-encoded)
filenamestringFilename (URL-encoded)

Response (JSON)

Shared file metadata

FieldTypeDescription
successbooleanQuery success status
fileobjectFile details
file.idintegerFile ID
file.filenamestringFile name
file.filesizeintegerFile size in bytes
file.short_descriptionstringBrief description
file.long_descriptionstringExtended description
file.created_atstringUpload timestamp (ISO 8601)
file.virus_scannedbooleanWhether virus scan was performed
file.virus_scan_resultstring|nullScan result if scanned
file.file_area_idintegerAssociated file area ID
file.area_tagstringTag of the file area
file.area_descriptionstringDescription of the file area
file.domainstringDomain of the file area
share_infoobjectShare metadata
share_info.share_idintegerShare record ID
share_info.shared_bystringUsername of the user who created the share
share_info.created_atstringShare creation timestamp (ISO 8601)
share_info.expires_atstring|nullShare expiry timestamp; null for no expiry
share_info.access_countintegerNumber of times the share has been accessed
share_info.share_urlstringFull URL of the share link
share_info.is_logged_inbooleanWhether the requesting user is authenticated

Error Responses

StatusDescription
404Share not found, expired, or feature disabled

DELETE /api/files/shares/{shareId}

Requires authentication

Deletes a share link, preventing further access via that share. Only the share creator or admins can revoke. Returns success confirmation with localized message code.

Path Parameters

NameTypeDescription
shareIdintegerShare ID to revoke

Response (JSON)

Revocation result

FieldTypeDescription
successbooleanRevocation success status
message_codestringLocalization key for success message

Error Responses

StatusDescription
404Share not found or user lacks permission to revoke

POST /api/files/upload

Requires authentication

Accepts multipart form data to upload a file to a specified file area. Requires file_area_id, short_description, and optionally long_description. Validates file area access permissions, upload permissions (read-only areas rejected), and user quotas. May deduct upload costs from user account if configured. Returns file metadata including ID, hash, and size on success.

Request Body (JSON)

Multipart form data with file and metadata

FieldTypeRequiredDescription
filefileYesBinary file to upload
file_area_idintegerYesTarget file area ID
short_descriptionstringYesBrief file description (max 255 chars)
long_descriptionstringNoExtended description

Response (JSON)

Uploaded file metadata and transaction details

FieldTypeDescription
successbooleanOperation success flag
file_idintegerID of uploaded file
filenamestringStored filename
file_hashstringSHA-256 hash of file
file_sizeintegerFile size in bytes
upload_cost_chargedbooleanWhether cost was deducted
upload_costintegerCost deducted (if any)

Error Responses

StatusDescription
404File areas feature disabled
400Missing required fields or invalid file area ID
403Access denied, read-only area, or quota exceeded
413File too large

POST /api/files/add-link

Requires authentication

Creates a file entry pointing to an external URL instead of uploading binary data. Requires file_area_id, valid URL, and short_description. Validates file area access and upload permissions. Optionally deducts link-creation costs. URL must pass FILTER_VALIDATE_URL validation.

Request Body (JSON)

JSON object with link metadata

FieldTypeRequiredDescription
file_area_idintegerYesTarget file area ID
urlstringYesExternal URL (must be valid)
file_namestringNoDisplay name for link
short_descriptionstringYesBrief description (max 255 chars)
long_descriptionstringNoExtended description

Response (JSON)

Created link entry metadata

FieldTypeDescription
successbooleanOperation success flag
file_idintegerID of created link entry
urlstringStored URL
upload_cost_chargedbooleanWhether cost was deducted

Error Responses

StatusDescription
404File areas feature disabled or file area not found
400Invalid URL or missing required fields
403Access denied or read-only area

POST /api/files/fetch-url-meta

Requires authentication

Server-side metadata scraper to avoid CORS issues. Extracts page title (short_description) and og:description (long_description) from HTML. Special handling for YouTube via oEmbed API. Returns empty strings if metadata unavailable. Timeout: 8 seconds per request.

Request Body (JSON)

JSON object with URL

FieldTypeRequiredDescription
urlstringYesURL to fetch metadata from

Response (JSON)

Extracted metadata from URL

FieldTypeDescription
short_descriptionstringPage title (max 255 chars)
long_descriptionstringog:description or meta description
og_image_urlstringog:image URL if available

Error Responses

StatusDescription
404File areas feature disabled
400Invalid or missing URL

DELETE /api/files/{id}/delete

Requires authentication

Removes a file entry and associated data. Owner or admin required. ISO-backed files (source_type='iso_import') cannot be deleted by non-admins. Returns success message on deletion.

Path Parameters

NameTypeDescription
idintegerFile ID to delete

Response (JSON)

Deletion confirmation

FieldTypeDescription
successbooleanDeletion successful
message_codestringLocalization key for success message

Error Responses

StatusDescription
404File areas feature disabled
403Access denied or ISO-backed file cannot be deleted

PUT /api/files/{id}/rename

Requires authentication

Updates file metadata. filename, short_description, long_description, url, and file_area_id are all optional; omit to skip update. If provided, filename and short_description must be non-empty. Only admins may move files (file_area_id). ISO-backed files cannot be renamed or moved, but descriptions may be edited.

Path Parameters

NameTypeDescription
idintegerFile ID to edit

Request Body (JSON)

JSON object with optional fields to update

FieldTypeRequiredDescription
filenamestringNoNew filename (non-empty if provided)
short_descriptionstringNoNew short description (non-empty if provided)
long_descriptionstringNoNew long description (empty string clears it)
urlstringNoNew URL for link entries
file_area_idintegerNoMove to different area (admin only)

Response (JSON)

Update confirmation with the fields that were changed

FieldTypeDescription
successbooleanUpdate successful
filenamestringNew filename (only present if filename was updated)
short_descriptionstringNew short description (only present if description was updated)
long_descriptionstring|nullNew long description (only present if description was updated)
file_area_idintegerNew file area ID (only present if file was moved; admin only)
urlstringNew URL (only present if URL was updated; admin only)

Error Responses

StatusDescription
404File areas feature disabled or file not found
400Filename or short_description empty when provided
403Access denied, non-admin move attempt, or ISO-backed file rename/move

POST /api/files/{id}/scan

Requires authentication

Initiates asynchronous virus scan via admin daemon. Admin-only endpoint. Returns scan result (clean/infected), signature if infected, and scanned flag. Requires VIRUS_SCAN_DISABLED != 'true' in config.

Path Parameters

NameTypeDescription
idintegerFile ID to scan

Response (JSON)

Scan result from ClamAV

FieldTypeDescription
successbooleanScan initiated successfully
resultstringScan result: 'clean', 'infected', or null
signaturestringMalware signature if infected
scannedbooleanWhether scan completed

Error Responses

StatusDescription
404File areas feature disabled
403Admin access required or virus scanning disabled
500Scan daemon communication failed

PUT /api/files/{id}/scan-status

Requires authentication

Sets scan status to not_scanned, clean, or infected without running ClamAV. Admin-only. Optionally stores signature for infected status. Updates virus_scanned, virus_scan_result, virus_signature, and virus_scanned_at fields.

Path Parameters

NameTypeDescription
idintegerFile ID to update

Request Body (JSON)

JSON object with scan status override

FieldTypeRequiredDescription
statusstringYesOne of: 'not_scanned', 'clean', 'infected'
signaturestringNoMalware signature (used if status='infected')

Response (JSON)

Status update confirmation

FieldTypeDescription
successbooleanStatus updated successfully

Error Responses

StatusDescription
404File areas feature disabled or file not found
400Invalid scan status value
403Admin access required

GET /api/files/{id}/comments

Requires authentication

Retrieves all comments for a file via its file area's linked comment echoarea. Uses FILEREF kludge (new and legacy formats) and subject matching to build thread tree. Returns empty array if no comment echoarea linked. Includes from_name, subject, message_text, and date_written for each comment.

Path Parameters

NameTypeDescription
idintegerFile ID to fetch comments for

Response (JSON)

Threaded comment messages

FieldTypeDescription
enabledbooleanWhether comments are enabled for this file
commentsarrayThreaded tree of top-level comment objects
comments[].idintegerEchomail message ID
comments[].from_namestringName of the commenter
comments[].date_writtenstringMessage timestamp (ISO 8601)
comments[].bodystringComment text (tearline stripped)
comments[].levelintegerNesting depth (0 = top-level, max 2)
comments[].childrenarrayNested reply objects (same structure, empty at level 2)
totalintegerTotal comment count (flat, across all levels)

Error Responses

StatusDescription
404File not found or file areas feature disabled

POST /api/files/{id}/comments

Requires authentication

Creates a comment on a file in its linked comment echo area. If no comment thread exists, one is created automatically. The file area must have a comment echo area configured. Respects sysop-only restrictions on the comment area. Supports optional reply threading via reply_to_id.

Path Parameters

NameTypeDescription
idintegerFile ID

Request Body (JSON)

Comment data

FieldTypeRequiredDescription
bodystringYesComment text (non-empty)
reply_to_idintegerNoID of message to reply to within the comment thread

Response (JSON)

Comment creation result with message details

FieldTypeDescription
successbooleanOperation success status
message_idintegerID of created comment message

Error Responses

StatusDescription
400Comment body is required or empty
403Comments not enabled for file area, or user lacks permission (sysop-only area)
404File not found

Freq Log

MethodPathAuthSummary
GET/admin/api/freq-logYesQuery file request frequency log with filtering.

GET /admin/api/freq-log

Requires authentication

Paginated admin endpoint for viewing file request logs. Supports filtering by requesting node, filename, served status, and source. Returns paginated results with total count. Requires admin authentication.

Query Parameters

NameTypeRequiredDescription
pageintegerNoPage number (default 1)
nodestringNoFilter by requesting node (partial match)
filenamestringNoFilter by filename (partial match)
servedstringNoFilter by served status ('0' or '1')
sourcestringNoFilter by request source

Response (JSON)

Paginated frequency log entries

FieldTypeDescription
successbooleanOperation success indicator
entriesarrayLog entries
entries[].idintegerLog entry ID
entries[].requested_atstringISO 8601 timestamp of the request
entries[].requesting_nodestringFTN address of the requesting node
entries[].filenamestringFilename that was requested
entries[].servedbooleanWhether the file was served
entries[].deny_reasonstring|nullReason for denial (if not served)
entries[].file_sizeinteger|nullSize of the served file in bytes
entries[].sourcestringSource of the file request
totalintegerTotal matching entries across all pages
pageintegerCurrent page number
per_pageintegerEntries per page (50)

Error Responses

StatusDescription
401Authentication required
403Admin privileges required

I18n

MethodPathAuthSummary
GET/api/i18n/catalogYesFetch i18n translation catalogs for specified namespaces.

GET /api/i18n/catalog

Requires authentication

Returns localized translation catalogs for one or more namespaces. Supports lazy loading of specific namespaces and locale resolution based on query parameter, user preferences, or system default. Persists resolved locale for the session.

Query Parameters

NameTypeRequiredDescription
localestringNoRequested locale code (e.g., 'en', 'de'). Falls back to user preference or system default if not provided
nsstringNoComma-separated list of namespace names to load (default: 'common'). Example: 'common,errors,admin'

Response (JSON)

Localized translation catalogs

FieldTypeDescription
successbooleanWhether catalogs were successfully loaded
localestringThe resolved locale code used for translations
default_localestringThe system default locale
catalogsobjectObject keyed by namespace name (e.g. common, errors); each value is an object mapping translation keys to their localized string values

Error Responses

StatusDescription
500Failed to load translation catalogs

Interests

MethodPathAuthSummary
GET/api/interests/NoRetrieve all active interests with subscription status.
POST/api/interests/{id}/subscribeYesSubscribe authenticated user to an interest with optional echo area selection.
POST/api/interests/{id}/unsubscribeYesUnsubscribe authenticated user from an interest or specific echo areas.
POST/api/interests/{id}/manage-areasYesReplace user's subscribed echo areas within an interest.
GET/api/interests/{id}/echoareasNoList echo areas belonging to an interest with optional subscription status.
GET/api/interests/{id}/statsYesGet message statistics for an interest's echo areas.
GET/api/interests/{id}/messagesYesGet paginated echomail messages from an interest's echo areas.

GET /api/interests/

Public

Returns a list of all active interests. When the request is authenticated, each interest includes a subscribed boolean indicating whether the current user is subscribed. When unauthenticated, all interests have subscribed: false. Feature can be disabled via ENABLE_INTERESTS environment variable.

Response (JSON)

List of active interests

FieldTypeDescription
interestsarrayArray of interest objects with subscription status
interests[].idintegerInterest ID
interests[].namestringInterest name
interests[].slugstringURL-safe slug
interests[].descriptionstring|nullInterest description
interests[].sort_orderintegerDisplay sort order
interests[].is_activebooleanWhether the interest is active
interests[].echoarea_countintegerNumber of echo areas in this interest
interests[].filearea_countintegerNumber of file areas in this interest
interests[].subscriber_countintegerNumber of subscribers
interests[].subscribedbooleanTrue if the authenticated user is subscribed

Error Responses

StatusDescription
404Interests feature is disabled (ENABLE_INTERESTS != 'true')

POST /api/interests/{id}/subscribe

Requires authentication

Subscribes the authenticated user to an interest. If echoarea_ids array is provided in the request body, subscribes only to those specific echo areas within the interest; otherwise subscribes to all echo areas. Requires the interests feature to be enabled. Returns success status and subscription confirmation.

Path Parameters

NameTypeDescription
idintegerInterest ID

Request Body (JSON)

Optional echo area selection

FieldTypeRequiredDescription
echoarea_idsinteger[]NoArray of echo area IDs to subscribe to within this interest. If omitted, subscribes to all areas.

Response (JSON)

Subscription confirmation

FieldTypeDescription
successbooleanOperation succeeded
subscribedbooleanUser is now subscribed to the interest

Error Responses

StatusDescription
404Interest not found or interests feature disabled

POST /api/interests/{id}/unsubscribe

Requires authentication

Unsubscribes the authenticated user from an interest. If echoarea_ids array is provided, removes subscription only from those specific echo areas; otherwise removes all subscriptions to the interest. Returns success status and whether user remains subscribed to any areas in the interest.

Path Parameters

NameTypeDescription
idintegerInterest ID

Request Body (JSON)

Optional selective unsubscription

FieldTypeRequiredDescription
echoarea_idsinteger[]NoArray of echo area IDs to unsubscribe from. If omitted, unsubscribes from all areas.

Response (JSON)

Unsubscription confirmation

FieldTypeDescription
successbooleanOperation succeeded
subscribedbooleanUser still has active subscriptions in this interest

Error Responses

StatusDescription
404Interest not found or interests feature disabled

POST /api/interests/{id}/manage-areas

Requires authentication

Replaces the user's entire set of subscribed echo areas for an interest with the provided list. Passing an empty array fully unsubscribes the user from the interest. This is an atomic replace operation, not additive.

Path Parameters

NameTypeDescription
idintegerInterest ID

Request Body (JSON)

New set of echo area subscriptions

FieldTypeRequiredDescription
wanted_echoarea_idsinteger[]YesArray of echo area IDs to subscribe to. Empty array unsubscribes from the interest entirely.

Response (JSON)

Management confirmation

FieldTypeDescription
successbooleanOperation succeeded
subscribedbooleanUser still has active subscriptions in this interest

Error Responses

StatusDescription
404Interest not found or interests feature disabled

GET /api/interests/{id}/echoareas

Public

Returns all echo areas associated with an interest, including tag, domain, description, and message count. If authenticated, includes a subscribed boolean for each area indicating the user's subscription status. Public endpoint respecting the interests feature flag.

Path Parameters

NameTypeDescription
idintegerInterest ID

Response (JSON)

List of echo areas in the interest

FieldTypeDescription
echoareasobject[]Array of echo area objects
echoareas[].echoarea_idintegerEcho area ID
echoareas[].tagstringEcho area tag
echoareas[].domainstringNetwork domain
echoareas[].descriptionstring|nullEcho area description
echoareas[].message_countintegerTotal message count
echoareas[].subscribedboolean(authenticated only) Whether the user is subscribed to this area

Error Responses

StatusDescription
404Interest not found or interests feature disabled

GET /api/interests/{id}/stats

Requires authentication

Returns aggregated message counts across all echo areas in an interest that the user is subscribed to. Includes total, recent (last 24h), unread, area count, and filter-specific counts (all, unread, read, to_me, saved, drafts). Respects sysop-only area restrictions for non-admin users.

Path Parameters

NameTypeDescription
idintegerInterest ID

Response (JSON)

Aggregated message statistics

FieldTypeDescription
totalintegerTotal messages in subscribed areas
recentintegerMessages from last 24 hours
unreadintegerUnread messages for user
areasintegerNumber of subscribed echo areas
filter_countsobjectCounts by filter
filter_counts.allintegerTotal messages
filter_counts.unreadintegerUnread messages
filter_counts.readintegerRead messages
filter_counts.tomeintegerMessages addressed to me
filter_counts.savedintegerSaved messages
filter_counts.draftsintegerDraft messages

Error Responses

StatusDescription
404Interest not found, inactive, or user has no subscriptions

GET /api/interests/{id}/messages

Requires authentication

Returns paginated echomail messages from all echo areas belonging to the interest that the user is subscribed to. Supports sorting (date_desc, date_asc, subject, author) and filtering (all, unread, read, tome, saved, drafts). Pagination defaults to page 1.

Path Parameters

NameTypeDescription
idintegerInterest ID

Query Parameters

NameTypeRequiredDescription
pageintegerNoPage number (default: 1)
sortstringNoSort order: date_desc, date_asc, subject, author (default: date_desc)
filterstringNoMessage filter: all, unread, read, tome, saved, drafts (default: all)

Response (JSON)

Paginated message results

FieldTypeDescription
messagesobject[]Array of echomail message objects (same shape as GET /api/messages/echomail)
paginationobjectPagination metadata
pagination.pageintegerCurrent page number
pagination.limitintegerMessages per page
pagination.totalintegerTotal matching messages
pagination.pagesintegerTotal number of pages

Error Responses

StatusDescription
404Interest not found, inactive, or interests feature disabled

Markdown Images

MethodPathAuthSummary
GET/api/markdown-imagesYesList markdown images uploaded by the authenticated user.
POST/api/markdown-imagesYesUpload a markdown image for the authenticated user.

GET /api/markdown-images

Requires authentication

Retrieves all markdown images associated with the authenticated user. Returns image metadata including filename, accessible URL, and creation timestamp. URLs are constructed using either a user-specific slug or file hash. Requires authentication.

Response (JSON)

JSON object containing success flag and array of image objects.

FieldTypeDescription
successbooleanAlways true on success
imagesarrayArray of image objects
images[].filenamestringOriginal filename
images[].urlstringPublic URL for embedding in markdown
images[].created_atstringISO 8601 upload timestamp

Error Responses

StatusDescription
500Failed to load images from storage

POST /api/markdown-images

Requires authentication

Accepts multipart image upload (JPEG, PNG, GIF, WebP) up to 5MB (configurable). Stores image and generates a user-specific URL slug. Returns the public URL for embedding in markdown. Validates MIME type and file size before storage. Requires authentication.

Request Body (JSON)

Multipart form data with image file.

FieldTypeRequiredDescription
imagefileYesImage file (JPEG, PNG, GIF, or WebP)

Response (JSON)

JSON object with upload success, public URL, and original filename.

FieldTypeDescription
successbooleanTrue if upload succeeded
urlstringPublic URL for accessing the uploaded image
filenamestringOriginal filename as provided by client

Error Responses

StatusDescription
400Upload failed, unsupported MIME type, or file exceeds size limit
500Failed to store image to filesystem

Media

MethodPathAuthSummary
GET/api/media/rawNoProxy and stream raw media files from external URLs.
GET/api/media/embedNoResolve media URLs to embeddable HTML for supported providers.

GET /api/media/raw

Public

Fetches and streams audio/music files (XM, IT, S3M, MOD, SID, MIDI, etc.) from external public URLs via CURL. Validates URL scheme (HTTP/HTTPS), file extension, and host to prevent SSRF attacks. Enforces 8MB size limit. Returns 404 if URL is invalid or media unavailable.

Query Parameters

NameTypeRequiredDescription
urlstringYesPublic HTTP(S) URL to media file with allowed extension

Response (binary)

Raw proxied media file bytes

HeaderValue
Content-TypeMIME type reported by the upstream server (e.g. audio/x-mod, application/octet-stream)
Content-LengthFile size in bytes
Content-Dispositioninline; filename="<basename>"
Cache-Controlpublic, max-age=86400

Error Responses

StatusDescription
404Invalid URL, disallowed extension, private host, or media not found

GET /api/media/embed

Public

Detects media provider (YouTube, Vimeo, etc.) from URL and returns embed HTML if provider is enabled. Respects global media player configuration and per-provider settings. Returns unknown type with empty embed_html if URL is invalid, provider disabled, or resolution fails. No authentication required.

Query Parameters

NameTypeRequiredDescription
urlstringYesHTTP(S) URL to resolve for media embedding

Response (JSON)

JSON object with media type, provider name, and embed HTML.

FieldTypeDescription
typestringMedia type (e.g., 'video', 'audio') or 'unknown'
providerstringnull
embed_htmlstringHTML embed code or empty string if unavailable

MeshCore

Bridge-facing endpoints authenticated with a per-node Bearer token (Authorization: Bearer <api_key>).

MethodPathAuthSummary
POST/api/meshcore/contactBearerReport a companion contact from a MeshCore bridge.
GET/api/meshcore/pending-commandsBearerPoll for device commands queued for this bridge (e.g. remove_contact).
POST/api/meshcore/commands/{id}/ackBearerAcknowledge that a device command has been sent to the radio.

POST /api/meshcore/contact

Requires Bearer token (packet-BBS node API key)

Called by the MeshCore bridge when a stored companion contact is received from the radio. Creates or updates a meshcore_contacts row. If a user has already registered a prefix-only contact matching this key, that row is claimed and updated with the full key.

Request Body (JSON)

FieldTypeRequiredDescription
pub_key_hexstringYesFull 64-char lowercase hex public key
bridge_node_idstringYesBridge node ID (from SelfInfo)
namestringNoContact name from radio
adv_typestringNoAdvertisement type
latitudefloat|nullNoGPS latitude
longitudefloat|nullNoGPS longitude

Response (JSON)

FieldTypeDescription
successbooleanOperation success flag
idintegerContact record ID
actionstringinserted, updated, or claimed

Error Responses

StatusDescription
400Missing or invalid pub_key_hex
401Missing or invalid Bearer token

GET /api/meshcore/pending-commands

Requires Bearer token (packet-BBS node API key)

Returns unexecuted device commands queued for this bridge node. The bridge polls this endpoint on the same interval as pending messages and executes each command against the radio.

Query Parameters

ParameterRequiredDescription
bridge_node_idYesFull 64-char hex public key of the bridge node

Response (JSON)

FieldTypeDescription
commandsarrayList of pending command objects
commands[].idintegerCommand record ID (used for ACK)
commands[].command_typestringCommand type, e.g. remove_contact
commands[].payloadobjectCommand-specific data (see below)

remove_contact payload fields

FieldTypeDescription
pub_key_fullstringFull 64-char hex public key of the contact to remove

POST /api/meshcore/commands/{id}/ack

Requires Bearer token (packet-BBS node API key)

Marks a device command as executed. The bridge calls this after dispatching the command to the radio, regardless of whether the radio acknowledged it.

Request Body (JSON)

FieldTypeRequiredDescription
bridge_node_idstringYesFull 64-char hex public key of the bridge node

Response (JSON)

FieldTypeDescription
successbooleanTrue if the command was found and marked executed

Messages

MethodPathAuthSummary
GET/api/messages/recentYesRetrieve recent netmail and echomail messages for the user.
GET/api/messages/netmailYesRetrieve paginated netmail messages for the authenticated user.
GET/api/messages/netmail/statsYesGet netmail statistics (total and unread message counts).
GET/api/messages/netmail/{id}YesRetrieve a single netmail message by ID with full details.
GET/api/messages/netmail/{id}/conversationYesRetrieve a conversation thread containing a specific netmail message.
DELETE/api/messages/netmail/{id}YesDelete a netmail message by ID.
GET/api/messages/netmail/{id}/downloadYesDownload a netmail message as a plain text file with headers.
POST/api/messages/netmail/{id}/editYesEdit netmail message metadata (art format, charset).
POST/api/messages/netmail/bulk-deleteYesDelete multiple netmail messages in bulk.
POST/api/messages/netmail/readYesMark multiple netmail messages as read in bulk.
GET/api/messages/echomailYesList echomail messages from subscribed areas with filtering.
POST/api/messages/echomail/readYesMark multiple echomail messages as read in bulk.
POST/api/messages/echomail/deleteYesDelete multiple echomail messages (admin only).
POST/api/messages/echomail/ignore-rulesYesCreate an echomail ignore rule for the authenticated user.
GET/api/messages/echomail/statsYesGet aggregate echomail statistics for all areas.
GET/api/messages/echomail/stats/{echoarea}YesGet echomail statistics for a specific echo area.
GET/api/messages/echomail/message/{id}YesRetrieve a specific echomail message by ID.
GET/api/messages/echomail/message/{id}/conversationYesGet conversation thread for an echomail message.
POST/api/messages/echomail/{id}/save-adYesSave an ANSI echomail message to the ad library (admin only).
GET/api/messages/echomail/{id}/downloadYesDownload an echomail message as a text file.
POST/api/messages/echomail/{id}/editYesEdit echomail message metadata (admin only).
GET/api/messages/echomail/{echoarea}YesRetrieve echomail messages from a specific echo area with pagination and filtering.
GET/api/messages/echomail/{echoarea}/{id}YesRetrieve a single echomail message by ID with full content and metadata.
POST/api/messages/sendYesSend a netmail or echomail message with optional attachments and formatting.
GET/api/messages/markdown-supportYesCheck markdown support and posting name policy for a destination.
POST/api/messages/markdown-previewYesRender markdown text to HTML for preview in compose UI.
POST/api/messages/draftYesSave a message draft for later completion and sending.
GET/api/messages/draftsYesRetrieve authenticated user's draft messages.
GET/api/messages/drafts/{id}YesRetrieve a specific draft message by ID.
DELETE/api/messages/drafts/{id}YesDelete a draft message.
GET/api/messages/templatesYesList message templates for authenticated user.
GET/api/messages/templates/{id}YesRetrieve a single message template with full body.
POST/api/messages/templatesYesCreate or update a message template.
DELETE/api/messages/templates/{id}YesDelete a message template.
GET/api/messages/searchYesSearch messages with optional field-specific and date filters.
POST/api/messages/{type}/{id}/readYesMark a message as read for the authenticated user.
POST/api/messages/{type}/{id}/saveYesSave a message for later viewing.
DELETE/api/messages/{type}/{id}/saveYesRemove a message from the authenticated user's saved collection.
POST/api/messages/{type}/{id}/forward-emailYesForward a message to user's email address.
GET/api/messages/echomail/delete-testNoTest endpoint for message delete functionality.
POST/api/messages/echomail/{id}/shareYesCreate a share link for an echomail message.
GET/api/messages/echomail/{id}/sharesYesList share links for an echomail message.
DELETE/api/messages/echomail/{id}/shareYesRevoke a shared echomail message link.
POST/api/messages/echomail/{id}/share/friendly-urlYesGenerate a friendly URL slug for an existing message share.
POST/api/messages/echomail/{id}/share/imageYesUpload an OG preview image for an existing message share.
DELETE/api/messages/echomail/{id}/share/imageYesRemove the OG preview image from an existing message share.
POST/api/messages/echomail/{id}/share-summaryYesGenerate an AI summary for a shared echomail message.
GET/api/messages/shared/{area}/{slug}YesRetrieve a shared echomail message by friendly URL slug.
GET/api/messages/shared/{shareKey}YesRetrieve a shared message by share key.
POST/api/messages/ai-assistYesGenerate AI-assisted response for echomail or netmail messages.

GET /api/messages/recent

Requires authentication

Fetches the 10 most recent messages for the authenticated user, combining netmail (direct messages) and echomail (echo area messages). Only includes echomail from areas the user is subscribed to. Results are ordered by date written (newest first). Includes echoarea tag and color for echomail messages.

Response (JSON)

JSON object containing array of recent messages

FieldTypeDescription
messagesarray of objectsArray of recent message objects
messages[].idintegerMessage ID
messages[].typestringMessage type: netmail or echomail
messages[].from_namestringSender display name
messages[].subjectstringMessage subject
messages[].date_writtenstringISO 8601 date the message was composed
messages[].echoareastring|nullEcho area tag (null for netmail)
messages[].echoarea_colorstring|nullEcho area display colour (null for netmail)

GET /api/messages/netmail

Requires authentication

Fetches netmail messages with support for pagination, filtering, sorting, and optional thread grouping. Supports multiple sort orders (date_desc, date_asc, subject, author) and filters (all, unread, etc.). Returns localized error messages.

Query Parameters

NameTypeRequiredDescription
pageintegerNoPage number (default: 1)
filterstringNoFilter type, e.g. 'all', 'unread' (default: 'all')
threadedbooleanNoGroup messages by thread (default: false)
sortstringNoSort order: date_desc, date_asc, subject, author (default: date_desc)

Response (JSON)

Paginated netmail message list

FieldTypeDescription
messagesarrayArray of netmail message objects (see shape below)
pagination.pageintegerCurrent page number
pagination.limitintegerMessages per page (from user setting, default 25)
pagination.totalintegerTotal message count matching the current filter
pagination.pagesintegerTotal number of pages

Netmail object

FieldTypeDescription
idintegerMessage ID
from_namestringSender display name (UTF-8 normalized)
from_addressstringSender FidoNet address (e.g. 1:123/456)
to_namestringRecipient display name
to_addressstringRecipient FidoNet address
subjectstringMessage subject; masked as "••••••••" for AreaFix/FileFix robot messages
date_receivedstringUTC timestamp when the message was stored server-side — reliable for display and sorting
date_writtenstringTimestamp from the FTN packet header — reflects when the sender composed the message; may be wrong or in the future if the remote clock is incorrect
user_idintegerID of the local user who owns (sent or received) this message
attributesintegerFTN message attribute bitmask (FTS-0001)
is_sentbooleanTrue if this message was sent by the local system
is_freqbooleanTrue if this is a file-request message
reply_to_idinteger|nullID of the message this is a reply to, or null
is_readinteger1 if the authenticated user has read this message, 0 otherwise
has_attachmentinteger1 if one or more file attachments exist for this message, 0 otherwise
is_savedinteger1 if the authenticated user has saved this message, 0 otherwise
replyto_addressstring|nullFidoNet address parsed from the REPLYTO kludge, if present
replyto_namestring|nullRecipient name parsed from the REPLYTO kludge, if present
from_domainstring|nullFTN domain name resolved from from_address, or null if unresolvable
to_domainstring|nullFTN domain name resolved from to_address, or null if unresolvable

GET /api/messages/netmail/stats

Requires authentication

Returns aggregate netmail statistics for the authenticated user, including total messages and unread count. Accounts for both received messages and sent messages (via system address). Uses message_read_status table to track read state. Handles cases where FidoNet address configuration is unavailable.

Response (JSON)

Netmail statistics

FieldTypeDescription
totalintegerTotal netmail messages (received + sent)
unreadintegerCount of unread messages

GET /api/messages/netmail/{id}

Requires authentication

Fetches a complete netmail message including kludge lines, REPLYTO header parsing, file attachments (if enabled), and edit permissions. Marks message as read via activity tracking. Includes parsed reply-to address and name extracted from message headers.

Path Parameters

NameTypeDescription
idintegerNetmail message ID

Response (JSON)

Complete netmail message with metadata

FieldTypeDescription
idintegerMessage ID
message_textstringMessage body
kludge_linesstringFidoNet kludge lines
replyto_addressstringParsed REPLYTO FidoNet address
replyto_namestringParsed REPLYTO recipient name
attachmentsarray of objectsFile attachments (empty array if feature disabled)
attachments[].idintegerFile record ID
attachments[].filenamestringOriginal filename
attachments[].filesizeintegerFile size in bytes
attachments[].short_descriptionstring|nullShort file description
attachments[].long_descriptionstring|nullExtended file description
attachments[].source_typestringOrigin: netmail_attachment, user, or fidonet
attachments[].statusstringApproval status: approved, pending, rejected, or quarantined
attachments[].created_atstringUTC timestamp when the file was stored
attachments[].area_tagstringFile area tag the attachment belongs to
attachments[].is_privatebooleanTrue if the file area is private
can_editbooleanWhether current user can edit this message

Error Responses

StatusDescription
404Message not found or user lacks access

GET /api/messages/netmail/{id}/conversation

Requires authentication

Fetches all messages in a conversation thread anchored by the specified message ID. Returns the full thread context including related messages. Useful for displaying message conversations in a threaded view.

Path Parameters

NameTypeDescription
idintegerNetmail message ID to anchor conversation

Response (JSON)

Full conversation thread, flattened in display order

FieldTypeDescription
messagesarrayNetmail message objects in the thread, flattened for display (same shape as the list endpoint, without is_saved)
unreadCountintegerNumber of unread messages in this thread
threadedbooleanAlways true for this endpoint
pagination.pageintegerAlways 1 (full thread is returned)
pagination.limitintegerNumber of messages returned
pagination.totalintegerTotal messages in the thread
pagination.pagesintegerAlways 1

Error Responses

StatusDescription
404Message not found or conversation has no messages

DELETE /api/messages/netmail/{id}

Requires authentication

Removes a netmail message. Only the message owner (user_id) can delete their own messages. Returns success confirmation or error if deletion fails or message not found.

Path Parameters

NameTypeDescription
idintegerNetmail message ID to delete

Response (JSON)

Deletion result

FieldTypeDescription
successbooleanDeletion success status
message_codestringLocalization key for success message

Error Responses

StatusDescription
404Message not found or user lacks permission to delete

GET /api/messages/netmail/{id}/download

Requires authentication

Retrieves a netmail message by ID and streams it as a downloadable .txt file. The message is formatted with standard email headers (From, To, Subject, Date) followed by the message body. Access is restricted to the message owner or admins. The filename is derived from the message subject and sanitized for Windows compatibility.

Path Parameters

NameTypeDescription
idintegerNetmail message ID

Response (JSON)

Plain text file download with CRLF line endings

FieldTypeDescription
bodystringEmail headers (From, To, Subject, Date) followed by message body

Error Responses

StatusDescription
404Message not found or user lacks access

POST /api/messages/netmail/{id}/edit

Requires authentication

Updates display metadata for a netmail message. Only the message owner or admins can edit. Supports setting art_format (ansi, amiga_ansi, petscii, plain, or empty) and message_charset (uppercase). When message_charset is changed to a non-empty value and raw_message_bytes are present, message_text is rebuilt from the raw bytes using the new charset.

Path Parameters

NameTypeDescription
idintegerNetmail message ID

Request Body (JSON)

Message metadata updates

FieldTypeRequiredDescription
art_formatstringNoDisplay format: 'ansi', 'amiga_ansi', 'petscii', 'plain', or empty string to clear
message_charsetstringNoCharacter set (e.g., 'UTF-8', 'CP437'). Empty string clears it.

Response (JSON)

JSON success response with update confirmation

FieldTypeDescription
successbooleanOperation succeeded

Error Responses

StatusDescription
400Invalid art_format or no fields provided
403User is not message owner and not admin
404Message not found

POST /api/messages/netmail/bulk-delete

Requires authentication

Deletes one or more netmail messages owned by the authenticated user. Only the message owner can delete their own messages. Returns count of successfully deleted messages. Requires non-empty message_ids array.

Request Body (JSON)

List of message IDs to delete

FieldTypeRequiredDescription
message_idsarrayYesNon-empty array of netmail message IDs

Response (JSON)

Deletion summary with localization support

FieldTypeDescription
successbooleanOperation succeeded
deletedintegerNumber of messages successfully deleted
totalintegerTotal messages requested for deletion
message_codestringLocalization key for UI message

Error Responses

StatusDescription
400message_ids missing, empty, or not an array

POST /api/messages/netmail/read

Requires authentication

Marks one or more netmail messages as read for the authenticated user. Uses upsert semantics — already-read messages are silently updated. Fires a BinkStream message_read event so other open tabs reflect the change immediately.

Request Body (JSON)

FieldTypeRequiredDescription
messageIdsarrayYesNon-empty array of netmail message IDs

Response (JSON)

FieldTypeDescription
successbooleanOperation succeeded
markedintegerNumber of messages processed
totalintegerTotal messages requested

Error Responses

StatusDescription
400messageIds missing, empty, or not an array
500Database error

GET /api/messages/echomail

Requires authentication

Retrieves paginated echomail messages from areas the user is subscribed to. Supports filtering (all, unread, etc.), sorting (date_desc, date_asc, subject, author), and optional threaded view. Returns localized error payloads on failure.

Query Parameters

NameTypeRequiredDescription
pageintegerNoPage number (default: 1)
filterstringNoFilter type: 'all', 'unread', etc. (default: 'all')
sortstringNoSort order: 'date_desc', 'date_asc', 'subject', 'author' (default: 'date_desc')
threadedbooleanNoEnable threaded view (default: false)

Response (JSON)

Paginated echomail message list

FieldTypeDescription
messagesarrayArray of echomail message objects (see shape below)
unreadCountintegerTotal unread messages across all subscribed areas matching the current filter
pagination.pageintegerCurrent page number
pagination.limitintegerMessages per page (from user setting, default 25)
pagination.totalintegerTotal message count matching the current filter
pagination.pagesintegerTotal number of pages
infostring(optional) Human-readable notice when the user has no subscriptions

Echomail object

FieldTypeDescription
idintegerMessage ID
from_namestringSender display name (UTF-8 normalized)
from_addressstringSender FidoNet address (e.g. 1:123/456)
to_namestringRecipient display name (often "All" for public posts)
subjectstringMessage subject
date_receivedstringUTC timestamp when the message was stored server-side — reliable for display and sorting
date_writtenstringTimestamp from the FTN packet header — reflects when the sender composed the message; may be wrong or in the future if the remote clock is incorrect
echoarea_idintegerID of the echo area this message belongs to
echoareastringEcho area tag (e.g. "FIDONEWS")
echoarea_colorstringHex color code configured for this echo area (e.g. "#28a745")
echoarea_domainstringDomain of the echo area (e.g. "lovlynet")
message_idstringFTN Message-ID kludge value from the original packet
reply_to_idinteger|nullID of the message this is a reply to, or null
art_formatstring|nullArt format hint: "ansi", "amiga_ansi", "petscii", or null (message-level value takes precedence over area default)
is_readinteger1 if the authenticated user has read this message, 0 otherwise
is_sharedinteger1 if an active share link exists for this message, 0 otherwise
is_savedinteger1 if the authenticated user has saved this message, 0 otherwise
replyto_addressstring|nullFidoNet address parsed from the REPLYTO kludge, if present
replyto_namestring|nullRecipient name parsed from the REPLYTO kludge, if present

Error Responses

StatusDescription
401Authentication required

POST /api/messages/echomail/read

Requires authentication

Marks specified echomail messages as read for the authenticated user and advances last_read_id watermarks per echoarea. Uses database transactions for consistency. Updates message_read_status table and user_echoarea_subscriptions watermarks.

Request Body (JSON)

List of message IDs to mark as read

FieldTypeRequiredDescription
messageIdsarrayYesNon-empty array of echomail message IDs

Response (JSON)

Read status update summary

FieldTypeDescription
successbooleanOperation succeeded
markedintegerNumber of messages marked as read

Error Responses

StatusDescription
400messageIds missing, empty, or not an array

POST /api/messages/echomail/delete

Requires authentication

Permanently deletes echomail messages from the database. Admin privileges required. Clears reply_to_id references and recalculates message_count for affected echoareas. Requires non-empty messageIds array.

Request Body (JSON)

List of message IDs to delete

FieldTypeRequiredDescription
messageIdsarrayYesNon-empty array of echomail message IDs

Response (JSON)

Deletion summary with localization support

FieldTypeDescription
successbooleanOperation succeeded
deletedintegerNumber of messages deleted

Error Responses

StatusDescription
400messageIds missing, empty, or not an array
403User lacks admin privileges

POST /api/messages/echomail/ignore-rules

Requires authentication

Adds a filter rule to automatically ignore echomail messages matching sender name, sender address, and/or subject keywords. Sender name is required; other fields optional. Validates field lengths (max 255 chars). Returns localized success message with sender name.

Request Body (JSON)

Ignore rule criteria

FieldTypeRequiredDescription
sender_namestringYesSender name to match (max 255 chars)
sender_addressstringNoSender address to match (max 255 chars)
subject_containsstringNoSubject substring to match (max 255 chars)

Response (JSON)

Rule creation confirmation with localization

FieldTypeDescription
successbooleanRule saved successfully
message_codestringLocalization key for UI message
message_paramsobjectLocalization parameters for message_code
message_params.sender_namestringSender name from the saved ignore rule

Error Responses

StatusDescription
400sender_name empty or field length exceeds 255 chars
500Failed to save rule to database

GET /api/messages/echomail/stats

Requires authentication

Returns overall echomail statistics for the authenticated user across all subscribed echo areas. Statistics include message counts and activity metrics. This endpoint must be called before area-specific stats endpoints.

Response (JSON)

Aggregate echomail statistics object

FieldTypeDescription
totalintegerTotal echomail message count across all subscribed areas
recentintegerMessages received in the last 24 hours
unreadintegerUnread message count across all subscribed areas
areasinteger|nullNumber of subscribed echo areas, or null for single-area queries
filter_countsobjectMessage counts broken down by filter type
filter_counts.allintegerTotal message count
filter_counts.unreadintegerUnread message count
filter_counts.readintegerRead message count
filter_counts.tomeintegerMessages addressed to the current user
filter_counts.savedintegerSaved message count
filter_counts.draftsintegerEchomail draft count

GET /api/messages/echomail/stats/{echoarea}

Requires authentication

Returns statistics for a single echo area. Non-admin users must be subscribed to the area. The echoarea parameter supports URL encoding and optional domain suffix (format: tag@domain). Admin users bypass subscription checks.

Path Parameters

NameTypeDescription
echoareastringEcho area tag, optionally with domain (e.g., 'GENERAL' or 'GENERAL@fidonet.org'). URL-encoded.

Response (JSON)

Statistics for the specified echo area

FieldTypeDescription
echoareastringThe requested echo area tag

Error Responses

StatusDescription
403User is not subscribed to this echo area (non-admin only)

GET /api/messages/echomail/message/{id}

Requires authentication

Fetches a single echomail message by its ID. Parses REPLYTO kludge lines from message text and includes reply-to address and name in response. Applies media permission resolution. Returns 404 if message not found.

Path Parameters

NameTypeDescription
idintegerThe echomail message ID

Response (JSON)

Complete echomail message object with parsed metadata

FieldTypeDescription
idintegerMessage ID
replyto_addressstringParsed REPLYTO address from kludge lines (if present)
replyto_namestringParsed REPLYTO name from kludge lines (if present)

Error Responses

StatusDescription
404Message not found

GET /api/messages/echomail/message/{id}/conversation

Requires authentication

Retrieves the full conversation thread containing the specified message, including all related messages in the thread. Returns 404 if the message is not found or has no conversation data.

Path Parameters

NameTypeDescription
idintegerThe echomail message ID to get conversation for

Response (JSON)

Full conversation thread, flattened in display order

FieldTypeDescription
messagesarrayEchomail message objects in the thread, flattened for display (same shape as the list endpoint, without replyto_address / replyto_name)
unreadCountintegerNumber of unread messages in this thread
threadedbooleanAlways true for this endpoint
pagination.pageintegerAlways 1 (full thread is returned)
pagination.limitintegerNumber of messages returned
pagination.totalintegerTotal messages in the thread
pagination.pagesintegerAlways 1

Error Responses

StatusDescription
404Message not found or no conversation data available

POST /api/messages/echomail/{id}/save-ad

Requires authentication

Converts an ANSI-formatted echomail message into an advertisement and saves it to the ad library. Admin privileges required. Validates that the message is ANSI-capable before saving. Creates an inactive ad with metadata extracted from the message.

Path Parameters

NameTypeDescription
idintegerThe echomail message ID to save as ad

Response (JSON)

Confirmation of ad creation

FieldTypeDescription
successbooleanTrue if ad was created

Error Responses

StatusDescription
403Admin privileges required
404Message not found
400Message is not ANSI-formatted or not suitable for ad library

GET /api/messages/echomail/{id}/download

Requires authentication

Exports an echomail message as a downloadable text file with RFC-style headers (From, To, Subject, Date, Area). Attempts to convert content to CP437 charset if iconv is available, otherwise uses UTF-8. Sanitizes filename for Windows compatibility.

Path Parameters

NameTypeDescription
idintegerThe echomail message ID to download

Response (JSON)

Message content as plain text file attachment

FieldTypeDescription
Content-Typeheadertext/plain; charset=utf-8 or charset=cp437
Content-Dispositionheaderattachment; filename=<sanitized_subject>.txt

Error Responses

StatusDescription
404Message not found

POST /api/messages/echomail/{id}/edit

Requires authentication

Updates message metadata fields (art_format, message_charset) for an echomail message. Admin privileges required. Accepts JSON body with optional art_format and message_charset fields. Valid art formats: '', 'ansi', 'amiga_ansi', 'petscii', 'plain'. When message_charset is changed to a non-empty value and raw_message_bytes are present, message_text is rebuilt from the raw bytes using the new charset.

Path Parameters

NameTypeDescription
idintegerThe echomail message ID to edit

Request Body (JSON)

Message metadata updates

FieldTypeRequiredDescription
art_formatstringNoArt format: '', 'ansi', 'amiga_ansi', 'petscii', or 'plain'
message_charsetstringNoCharacter set (uppercase, e.g., 'UTF-8', 'CP437')

Response (JSON)

Confirmation of metadata update

FieldTypeDescription
successbooleanTrue if update succeeded

Error Responses

StatusDescription
403Admin access required
400Invalid art_format or no fields to update
404Message not found

GET /api/messages/echomail/{echoarea}

Requires authentication

Fetches a paginated list of echomail messages from the specified echo area. Supports filtering by read/unread status, sorting by date or subject, and optional threaded view. The echoarea parameter supports URL-encoded names and optional @domain suffix. Tracks user activity for analytics.

Path Parameters

NameTypeDescription
echoareastringEcho area tag, optionally with @domain suffix (URL-encoded). Domain is extracted and used for filtering.

Query Parameters

NameTypeRequiredDescription
pageintegerNoPage number for pagination (default: 1)
filterstringNoFilter messages: 'all', 'unread', or 'read' (default: 'all')
threadedbooleanNoReturn messages in threaded view (default: false)
sortstringNoSort order: 'date_desc', 'date_asc', 'subject', or 'author' (default: 'date_desc')

Response (JSON)

Paginated list of echomail messages with metadata

FieldTypeDescription
messagesarrayArray of echomail message objects (see Echomail object in GET /api/messages/echomail)
pageintegerCurrent page number
total_pagesintegerTotal number of pages available

Error Responses

StatusDescription
401Authentication required

GET /api/messages/echomail/{echoarea}/{id}

Requires authentication

Fetches a complete echomail message including headers, body, kludge lines, and parsed REPLYTO information. Validates that the requested message belongs to the specified echo area and domain (case-insensitive). Extracts REPLYTO kludge data from both message text and kludge_lines fields. Applies media permission resolution for attachments.

Path Parameters

NameTypeDescription
echoareastringEcho area tag with optional @domain suffix (URL-encoded)
idintegerMessage ID

Response (JSON)

Complete echomail message object

FieldTypeDescription
idintegerMessage ID
echoareastringEcho area tag
domainstringDomain name
message_textstringMessage body
kludge_linesstringFidoNet kludge lines
replyto_addressstringParsed REPLYTO address (if present)
replyto_namestringParsed REPLYTO name (if present)

Error Responses

StatusDescription
401Authentication required
404Message not found or does not belong to specified echo area/domain

POST /api/messages/send

Requires authentication

Sends a message (netmail or echomail) with support for multiple charsets, markdown/plaintext markup, file attachments, and optional PGP payload handling. Enforces 16 KB FidoNet message body limit. For netmail, resolves attachment tokens to file paths. Supports crashmail flag and file request (FREQ) mode. Validates charset against a whitelist of safe values. Defaults to system address if no recipient specified for netmail.

Request Body (JSON)

Message composition payload

FieldTypeRequiredDescription
typestringYes'netmail' or 'echomail'
message_textstringYesMessage body (max 16384 bytes UTF-8)
markup_typestringNo'markdown' or null for plaintext (legacy: send_markdown boolean)
charsetstringNoTarget charset (UTF-8, CP437, CP850, ISO-8859-1, etc.; default: UTF-8)
to_addressstringNoNetmail recipient address (defaults to system address if empty)
attachment_tokenstringNo32-character hex token from attachment upload endpoint
crashmailbooleanNoSend as crashmail (netmail only)
is_freqbooleanNoMark as file request (netmail only)
pgp_modestringNoPGP handling mode: encrypt for netmail encryption or sign for echomail signing

Response (JSON)

Send result with message ID or error details

FieldTypeDescription
successbooleanSend status
message_idintegerID of sent message (if successful)

Error Responses

StatusDescription
400Message body exceeds 16 KB limit
401Authentication required
500Send failed (validation or database error)

GET /api/messages/markdown-support

Requires authentication

Determines whether markdown is allowed for a given netmail address, domain, or echomail area. Local echo areas always allow markdown. For remote areas, checks domain-level markdown configuration. Returns posting name policy (real_name or username) for the destination. Handles both local areas (NULL/empty domain) and remote areas with explicit domains.

Response (JSON)

Markdown support and posting policy for destination

FieldTypeDescription
allowedbooleanWhether markdown is permitted for this destination
posting_name_policystring'real_name' or 'username' — policy for sender name in message

Error Responses

StatusDescription
401Authentication required

POST /api/messages/markdown-preview

Requires authentication

Converts markdown-formatted text to HTML for live preview during message composition. Returns empty HTML for empty input. Uses the application's MarkdownRenderer for consistent formatting.

Request Body (JSON)

Markdown text to render

FieldTypeRequiredDescription
textstringYesMarkdown-formatted text

Response (JSON)

Rendered HTML output

FieldTypeDescription
htmlstringHTML representation of markdown input

Error Responses

StatusDescription
401Authentication required

POST /api/messages/draft

Requires authentication

Persists a partially-composed message as a draft. Accepts the same payload structure as the send endpoint but stores it for retrieval and editing later. Returns success status with a message code for UI feedback. Requires valid user session to associate draft with user account.

Request Body (JSON)

Draft message payload (same as send endpoint)

FieldTypeRequiredDescription
typestringYes'netmail' or 'echomail'
message_textstringYesDraft message body
to_addressstringNoRecipient address (netmail)
echoareastringNoTarget echo area (echomail)

Response (JSON)

Draft save result

FieldTypeDescription
successbooleanSave status
draft_idintegerID of saved draft
message_codestringUI message code (default: 'ui.compose.draft.saved_success')

Error Responses

StatusDescription
400Invalid draft payload
401Authentication required
500Failed to save draft or unable to resolve user session

GET /api/messages/drafts

Requires authentication

Fetches all draft messages for the authenticated user, optionally filtered by message type (netmail or echomail). Returns a list of draft metadata. Requires valid user session with either 'user_id' or 'id' field.

Query Parameters

NameTypeRequiredDescription
typestringNoFilter drafts by type: 'netmail' or 'echomail'. If omitted, returns all drafts.

Response (JSON)

Array of draft objects with metadata.

FieldTypeDescription
successbooleanIndicates successful retrieval.
draftsarray of objectsList of draft message objects.
drafts[].idintegerDraft ID.
drafts[].typestringDraft type: netmail or echomail.
drafts[].to_addressstring|nullFidoNet address of the recipient (netmail only).
drafts[].to_namestring|nullRecipient display name.
drafts[].echoareastring|nullEcho area tag (echomail only).
drafts[].subjectstring|nullMessage subject.
drafts[].message_textstring|nullDraft message body.
drafts[].reply_to_idinteger|nullID of the message this draft replies to, or null.
drafts[].created_atstringUTC timestamp when the draft was created.
drafts[].updated_atstringUTC timestamp of the last update.
drafts[].metaobject|nullAdditional metadata (e.g. cross-post area list), or null.

Error Responses

StatusDescription
500User ID cannot be resolved from session or draft retrieval failed.

GET /api/messages/drafts/{id}

Requires authentication

Fetches the full content of a single draft message for the authenticated user. Verifies ownership before returning. Returns 404 if draft does not exist or does not belong to the user.

Path Parameters

NameTypeDescription
idintegerThe draft message ID.

Response (JSON)

Single draft object with full content.

FieldTypeDescription
successbooleanIndicates successful retrieval.
draftobjectComplete draft message object.
draft.idintegerDraft ID.
draft.typestringDraft type: netmail or echomail.
draft.to_addressstring|nullFidoNet address of the recipient (netmail only).
draft.to_namestring|nullRecipient display name.
draft.echoareastring|nullEcho area tag (echomail only).
draft.subjectstring|nullMessage subject.
draft.message_textstring|nullDraft message body.
draft.reply_to_idinteger|nullID of the message this draft replies to, or null.
draft.created_atstringUTC timestamp when the draft was created.
draft.updated_atstringUTC timestamp of the last update.
draft.metaobject|nullAdditional metadata (e.g. cross-post area list), or null.

Error Responses

StatusDescription
404Draft not found or does not belong to authenticated user.
500User ID cannot be resolved or draft retrieval failed.

DELETE /api/messages/drafts/{id}

Requires authentication

Permanently deletes a draft message belonging to the authenticated user. Verifies ownership before deletion. Returns success with localized message code on successful deletion.

Path Parameters

NameTypeDescription
idintegerThe draft message ID to delete.

Response (JSON)

Deletion result with success status and message code.

FieldTypeDescription
successbooleanIndicates successful deletion.
message_codestringLocalization key for UI message (e.g., 'ui.drafts.deleted_success').

Error Responses

StatusDescription
500User ID cannot be resolved or deletion failed.

GET /api/messages/templates

Requires authentication

Retrieves all message templates owned by the authenticated user, optionally filtered by type. Requires valid license. Returns template metadata (id, name, type, subject, created_at) sorted by name.

Query Parameters

NameTypeRequiredDescription
typestringNoFilter templates by type: 'netmail' or 'echomail'. Templates with type='both' are always included.

Response (JSON)

Array of template metadata objects.

FieldTypeDescription
templatesarray of objectsList of template metadata objects.
templates[].idintegerTemplate ID.
templates[].namestringTemplate name (up to 100 characters).
templates[].typestringTemplate type: netmail, echomail, or both.
templates[].subjectstringTemplate subject line.
templates[].created_atstringUTC timestamp when the template was created.

Error Responses

StatusDescription
403Message templates require a valid registered license.

GET /api/messages/templates/{id}

Requires authentication

Fetches complete template content including body for the authenticated user. Verifies ownership and license validity. Returns 404 if template does not exist or does not belong to user.

Path Parameters

NameTypeDescription
idintegerThe template ID.

Response (JSON)

Complete template object with all fields.

FieldTypeDescription
templateobjectComplete template object.
template.idintegerTemplate ID.
template.namestringTemplate name (up to 100 characters).
template.typestringTemplate type: netmail, echomail, or both.
template.subjectstringTemplate subject line.
template.bodystringTemplate message body.
template.created_atstringUTC timestamp when the template was created.
template.updated_atstringUTC timestamp of the last modification.

Error Responses

StatusDescription
403Message templates require a valid registered license.
404Template not found or does not belong to authenticated user.

POST /api/messages/templates

Requires authentication

Creates a new template or updates an existing one (if 'id' is provided). Validates name (required, max 100 chars), type (netmail/echomail/both), subject, and body. Requires valid license. Returns template ID and success message code.

Request Body (JSON)

Template data to create or update.

FieldTypeRequiredDescription
namestringYesTemplate name (1–100 characters).
typestringNoTemplate type: 'netmail', 'echomail', or 'both' (default: 'both').
subjectstringNoTemplate subject line.
bodystringNoTemplate message body.
idintegerNoIf provided, updates existing template; otherwise creates new one.

Response (JSON)

Created or updated template with ID and success message.

FieldTypeDescription
successbooleanIndicates successful creation or update.
idintegerTemplate ID (new or existing).
message_codestringLocalization key (e.g., 'ui.compose.templates.saved').

Error Responses

StatusDescription
400Name is required, exceeds 100 characters, or validation failed.
403Message templates require a valid registered license.
404Template ID provided but not found or does not belong to user.

DELETE /api/messages/templates/{id}

Requires authentication

Permanently deletes a template belonging to the authenticated user. Requires valid license. Verifies ownership before deletion. Returns 404 if template does not exist.

Path Parameters

NameTypeDescription
idintegerThe template ID to delete.

Response (JSON)

Deletion confirmation with success status.

FieldTypeDescription
successbooleanIndicates successful deletion.
message_codestringLocalization key (e.g., 'ui.compose.templates.deleted').

Error Responses

StatusDescription
403Message templates require a valid registered license.
404Template not found or does not belong to authenticated user.

GET /api/messages/search

Requires authentication

Searches messages across drafts, netmail, and echomail. Supports general query (2+ chars) or advanced field-specific searches (from_name, subject, body, message_id, date_from, date_to). Date parameters must be YYYY-MM-DD format. Returns paginated results with message metadata.

Query Parameters

NameTypeRequiredDescription
qstringNoGeneral search query (minimum 2 characters if used alone).
typestringNoFilter by message type: 'netmail', 'echomail', or 'draft'.
echoareastringNoFilter by echo area name (URL-encoded).
from_namestringNoSearch sender name (minimum 2 characters).
subjectstringNoSearch subject line (minimum 2 characters).
bodystringNoSearch message body (minimum 2 characters).
message_idstringNoSearch by FidoNet message ID (minimum 2 characters).
date_fromstringNoStart date filter (YYYY-MM-DD format).
date_tostringNoEnd date filter (YYYY-MM-DD format).

Response (JSON)

Search results with message metadata and per-area counts.

FieldTypeDescription
messagesarrayArray of matching message objects
messages[].idintegerMessage ID
messages[].from_namestringSender name
messages[].from_addressstringSender FTN address
messages[].to_namestringRecipient name
messages[].subjectstringMessage subject
messages[].date_receivedstringServer receipt timestamp (ISO 8601)
messages[].date_writtenstringSender-written timestamp (ISO 8601)
messages[].echoarea_idinteger|nullEcho area ID (echomail only)
messages[].echoareastring|nullEcho area tag (echomail only)
messages[].echoarea_domainstring|nullEcho area domain (echomail only)
echoarea_countsarrayPer-area message counts (echomail searches only; empty for netmail/draft)
echoarea_counts[].tagstringEcho area tag
echoarea_counts[].domainstringEcho area domain
echoarea_counts[].message_countintegerNumber of matching messages in this area
filter_countsobjectCounts by read/saved status across all results (echomail only)
filter_counts.allintegerTotal matches
filter_counts.unreadintegerUnread matches
filter_counts.readintegerRead matches
filter_counts.tomeintegerMessages addressed to the current user
filter_counts.savedintegerSaved matches
filter_counts.draftsintegerAlways 0 (reserved)

Error Responses

StatusDescription
400Search query or field-specific search is less than 2 characters, or date format is invalid.

POST /api/messages/{type}/{id}/read

Requires authentication

Records that the authenticated user has read a specific message (echomail or netmail). Uses upsert logic to handle duplicate reads gracefully. Emits a real-time notification via BinkStream to sync read status across user tabs. Message type must be 'echomail' or 'netmail'.

Path Parameters

NameTypeDescription
typestringMessage type: 'echomail' or 'netmail'
idintegerMessage ID

Response (JSON)

Success response with optional real-time notification status

FieldTypeDescription
successbooleanOperation succeeded

Error Responses

StatusDescription
400Invalid message type (not 'echomail' or 'netmail')
500Failed to mark message as read or unable to resolve user session

POST /api/messages/{type}/{id}/save

Requires authentication

Adds a message to the authenticated user's saved messages collection. Idempotent—saving an already-saved message has no effect. Supports both echomail and netmail types.

Path Parameters

NameTypeDescription
typestringMessage type: 'echomail' or 'netmail'
idintegerMessage ID

Response (JSON)

Success response with localized message code

FieldTypeDescription
successbooleanOperation succeeded
message_codestringLocalization key: 'ui.api.messages.saved'

Error Responses

StatusDescription
400Invalid message type
500Failed to save message or unable to resolve user session

DELETE /api/messages/{type}/{id}/save

Requires authentication

Deletes a saved message entry for the authenticated user. Returns success even if the message was not previously saved (idempotent). Supports both echomail and netmail types.

Path Parameters

NameTypeDescription
typestringMessage type: 'echomail' or 'netmail'
idintegerMessage ID

Response (JSON)

Success or not-saved status with localization key

FieldTypeDescription
successbooleanTrue if unsaved; false if message was not saved
message_codestringLocalization key: 'ui.api.messages.unsaved' on success
error_codestringLocalization key on failure: 'errors.messages.unsave.not_saved'

Error Responses

StatusDescription
400Invalid message type
500Failed to unsave message or unable to resolve user session

POST /api/messages/{type}/{id}/forward-email

Requires authentication

Converts and sends an echomail or netmail message to the authenticated user's registered email address. Validates message ownership, email configuration, and message type. Requires user to have a valid email address on file. Supports both echomail (with area/domain context) and netmail types.

Path Parameters

NameTypeDescription
typestringMessage type: 'echomail' or 'netmail'
idintegerMessage ID to forward

Response (JSON)

Email forwarding confirmation.

FieldTypeDescription
successbooleanEmail sent successfully

Error Responses

StatusDescription
400User has no email address or invalid message type
404Message not found or user lacks access
503Email sending not configured on system

GET /api/messages/echomail/delete-test

Public

Debug endpoint that verifies the delete endpoint is accessible. Returns success status with a message code. No authentication required.

Response (JSON)

Success confirmation with message code

FieldTypeDescription
successbooleanAlways true
message_codestringLocalization key: 'ui.api.debug.delete_endpoint_accessible'

POST /api/messages/echomail/{id}/share

Requires authentication

Creates a shareable link for an echomail message with optional expiration and public/private visibility. Supports custom OpenGraph summaries for AI-generated previews. Returns share metadata including the generated share token.

Path Parameters

NameTypeDescription
idstringMessage ID

Request Body (JSON)

Share configuration

FieldTypeRequiredDescription
publicbooleanNoWhether share is publicly accessible (default: false)
expires_hoursintegerNoHours until share expires; null or ≤0 means no expiration
ai_og_summarystringNoCustom OpenGraph summary for preview

Response (JSON)

Share creation result with token and metadata

FieldTypeDescription
successbooleanWhether share was created

Error Responses

StatusDescription
400Invalid input or share creation failed
500Server error during share creation

GET /api/messages/echomail/{id}/shares

Requires authentication

Retrieves all active share links for a specific echomail message. Requires authentication. Returns array of shares with metadata including expiration and visibility settings.

Path Parameters

NameTypeDescription
idstringMessage ID

Response (JSON)

Share links for this message, split by ownership

FieldTypeDescription
successbooleanTrue on success
my_sharesarrayShare links created by the authenticated user
my_shares[].share_keystringUnique share token used in the share URL
my_shares[].share_urlstringFull share URL (friendly URL if slug is set, otherwise key-based)
my_shares[].has_friendly_urlbooleanTrue if a slug-based friendly URL is available
my_shares[].created_atstringISO 8601 timestamp when the share was created
my_shares[].expires_atstring|nullISO 8601 expiry timestamp, or null if non-expiring
my_shares[].is_publicbooleanWhether the share is publicly accessible
my_shares[].access_countintegerNumber of times the share URL has been accessed
my_shares[].last_accessed_atstring|nullISO 8601 timestamp of last access, or null
my_shares[].og_image_pathstring|nullServer path to custom OG preview image, or null
my_shares[].og_image_slugstring|nullSlug for the OG image URL, or null
my_shares[].top_referrersarrayReserved; currently always empty
other_sharesarrayActive public share links for this message created by other users
other_shares[].share_urlstringFull share URL
other_shares[].shared_by_usernamestringUsername of the user who created the share
other_shares[].created_atstringISO 8601 timestamp when the share was created
other_shares[].is_publicbooleanWhether the share is publicly accessible
other_shares[].top_referrersarrayReserved; currently always empty

Error Responses

StatusDescription
500Failed to load share links

DELETE /api/messages/echomail/{id}/share

Requires authentication

Deletes the share link for an echomail message, preventing further access via the share URL. The authenticated user must own the message. Returns success status or 404 if the message or share does not exist.

Path Parameters

NameTypeDescription
idintegerThe echomail message ID to revoke sharing for

Response (JSON)

Share revocation result

FieldTypeDescription
successbooleanWhether the share was successfully revoked

Error Responses

StatusDescription
404Message or share not found
500Failed to revoke share link

POST /api/messages/echomail/{id}/share/friendly-url

Requires authentication

Creates a human-readable slug for an already-shared echomail message, enabling access via /api/messages/shared/{area}/{slug} instead of the share key. The authenticated user must own the message.

Path Parameters

NameTypeDescription
idintegerThe echomail message ID to generate a slug for

Response (JSON)

Slug generation result

FieldTypeDescription
successbooleanWhether the slug was successfully generated
slugstringThe generated friendly URL slug

Error Responses

StatusDescription
404Message or share not found
500Cannot generate share slug for this message

POST /api/messages/echomail/{id}/share/image

Requires authentication

Uploads an image to use as the Open Graph preview (og:image) for an existing message share. The image is stored in the sharer's private file area under shared-messages/. Accepts a multipart form upload with field name image. Replaces any previously uploaded image for the same share.

Path Parameters

NameTypeDescription
idintegerThe echomail message ID whose share receives the image

Request (multipart/form-data)

FieldTypeDescription
imagefileImage file (JPG, PNG, GIF, etc.); maximum 5 MB

Response (JSON)

FieldTypeDescription
successbooleantrue on success
share_keystring32-char hex share key
og_image_slugstringFilename slug including extension (e.g. abc123….jpg); use with /shared-image/{og_image_slug}

Error Responses

StatusDescription
400No file uploaded, invalid MIME type, or file too large
404Share not found
500Server error storing the image

DELETE /api/messages/echomail/{id}/share/image

Requires authentication

Removes the Open Graph preview image from an existing message share and deletes the stored file.

Path Parameters

NameTypeDescription
idintegerThe echomail message ID whose share image is removed

Response (JSON)

FieldTypeDescription
successbooleantrue on success

Error Responses

StatusDescription
404Share not found
500Server error removing the image

POST /api/messages/echomail/{id}/share-summary

Requires authentication

Uses AI to generate a concise summary of an echomail message for sharing purposes. Requires the AI share summary feature to be enabled in BBS config. Returns the generated summary text or 403 if the feature is disabled.

Path Parameters

NameTypeDescription
idintegerThe echomail message ID to summarize

Response (JSON)

AI-generated summary result

FieldTypeDescription
successbooleanWhether the summary was successfully generated
summarystringThe AI-generated summary text

Error Responses

StatusDescription
403AI share summaries are not enabled
404Message not found
500Failed to generate summary

GET /api/messages/shared/{area}/{slug}

Requires authentication

Fetches a shared echomail message using its friendly URL slug and area. Authentication is required if the share has login restrictions. Returns 401 if login is required but user is not authenticated, or 404 if the share does not exist.

Path Parameters

NameTypeDescription
areastringThe message area identifier
slugstringThe friendly URL slug for the shared message

Response (JSON)

Shared message data

FieldTypeDescription
successbooleanWhether the message was successfully retrieved
messageobjectThe echomail or netmail message record
message.idintegerMessage ID
message.from_namestringSender name
message.to_namestringRecipient name
message.subjectstringMessage subject
message.message_textstringMessage body
message.date_writtenstringSender-written timestamp (ISO 8601)
message.date_receivedstringServer receipt timestamp (ISO 8601)
message.echoareastring|nullEcho area tag (echomail only)
message.echoarea_colorstring|nullEcho area color (echomail only)
message.echoarea_domainstring|nullEcho area domain (echomail only)
message.from_system_namestring|nullNodelist system name for sender address
share_infoobjectShare metadata
share_info.idintegerShare record ID
share_info.share_keystringShare token string
share_info.shared_bystringReal name (or username) of the user who shared the message
share_info.created_atstringShare creation timestamp (ISO 8601)
share_info.expires_atstring|nullShare expiry timestamp; null for no expiry
share_info.is_publicbooleanWhether share is publicly accessible
share_info.access_countintegerNumber of times the share has been accessed
share_info.ai_og_summarystring|nullAI-generated OpenGraph summary if provided
share_info.og_image_pathstring|nullServer path to OpenGraph preview image
share_info.og_image_slugstring|nullURL slug for OpenGraph preview image

Error Responses

StatusDescription
401Login required to access this shared message
404Shared message not found
500Failed to load shared message

GET /api/messages/shared/{shareKey}

Requires authentication

Fetches a shared message using its unique share key. Authentication is optional; if provided, the user context is used for access control. Returns 401 if the share requires login but user is not authenticated, or 404 if the share key is invalid.

Path Parameters

NameTypeDescription
shareKeystringThe unique share key for the message

Response (JSON)

Shared message data

FieldTypeDescription
successbooleanWhether the message was successfully retrieved
messageobjectThe echomail or netmail message record
message.idintegerMessage ID
message.from_namestringSender name
message.to_namestringRecipient name
message.subjectstringMessage subject
message.message_textstringMessage body
message.date_writtenstringSender-written timestamp (ISO 8601)
message.date_receivedstringServer receipt timestamp (ISO 8601)
message.echoareastring|nullEcho area tag (echomail only)
message.echoarea_colorstring|nullEcho area color (echomail only)
message.echoarea_domainstring|nullEcho area domain (echomail only)
message.from_system_namestring|nullNodelist system name for sender address
share_infoobjectShare metadata
share_info.idintegerShare record ID
share_info.share_keystringShare token string
share_info.shared_bystringReal name (or username) of the user who shared the message
share_info.created_atstringShare creation timestamp (ISO 8601)
share_info.expires_atstring|nullShare expiry timestamp; null for no expiry
share_info.is_publicbooleanWhether share is publicly accessible
share_info.access_countintegerNumber of times the share has been accessed
share_info.ai_og_summarystring|nullAI-generated OpenGraph summary if provided
share_info.og_image_pathstring|nullServer path to OpenGraph preview image
share_info.og_image_slugstring|nullURL slug for OpenGraph preview image

Error Responses

StatusDescription
401Login required to access this shared message
404Shared message not found
500Failed to load shared message

POST /api/messages/ai-assist

Requires authentication

Calls an AI assistant (OpenAI or Anthropic) to generate a response based on a user prompt and optional message context. Requires AI assistant to be enabled and configured via environment variables. Prompt is limited to 500 characters. Supports both echomail and netmail message types.

Request Body (JSON)

AI assistance request

FieldTypeRequiredDescription
promptstringYesUser prompt (max 500 chars)
message_idintegerNoOptional message ID for context
message_typestringNoMessage type: 'echomail' or 'netmail' (default: 'echomail')

Response (JSON)

AI assistant result with the generated reply and resulting credit information.

FieldTypeDescription
successbooleanTrue when the request completed successfully
responsestringAI-generated reply text
credits_usedintegerCredits charged for this AI request
balanceintegerUser's remaining credit balance after the request

Error Responses

StatusDescription
403AI assistant is disabled on this system
503AI assistant not configured (missing API keys)
400Prompt is empty or exceeds 500 character limit
402Insufficient credits for the AI request

Netmail

MethodPathAuthSummary
POST/api/netmail/attachment/uploadYesUpload a file for attachment to an outbound netmail message.

POST /api/netmail/attachment/upload

Requires authentication

Accepts a multipart file upload and stores it temporarily with a unique token. The token is returned and must be provided when sending the netmail. Enforces file size limits (default 10 MB, configurable via NETMAIL_ATTACHMENT_MAX_SIZE). Sanitizes filenames to alphanumeric characters, dots, hyphens, and underscores. Files are stored in data/netmail_attachments with token prefix.

Request Body (JSON)

Multipart form data with file upload

FieldTypeRequiredDescription
filefileYesFile to upload (max size configurable, default 10 MB)

Response (JSON)

Upload success with attachment token

FieldTypeDescription
successbooleanUpload status
tokenstring32-character hex token to reference file in send request
filenamestringSanitized original filename

Error Responses

StatusDescription
400No file provided, upload error, or file exceeds size limit
401Authentication required
500Server error creating attachment directory or moving file

Nodelist

MethodPathAuthSummary
GET/api/nodelist/nodeYesLook up a nodelist entry by exact FTN address.
GET/api/nodelist/searchYesSearch nodelist nodes by name, sysop, or location.

GET /api/nodelist/node

Requires authentication

Retrieves nodelist information for a given FTN address. If the exact address is not found and it is a point address (contains a dot), falls back to searching for the parent node. Returns null if no match found. Requires exact address match.

Query Parameters

NameTypeRequiredDescription
addressstringYesFTN address to look up (e.g., '1:123/456' or '1:123/456.789')

Response (JSON)

Nodelist entry or null if not found

FieldTypeDescription
successbooleanAlways true; check node field for null
nodeobject|nullNode object, or null if address not found
node.addressstringFull FTN node address
node.system_namestringSystem name from nodelist
node.locationstringNode location
node.domainstringNetwork domain

Error Responses

StatusDescription
400Missing or empty address parameter

GET /api/nodelist/search

Requires authentication

Searches the nodelist for nodes matching a query term against system name, sysop name, or location. Returns up to 10 results suitable for autocomplete interfaces. Requires a minimum query length of 2 characters; shorter queries return an empty result set.

Query Parameters

NameTypeRequiredDescription
qstringYesSearch term (minimum 2 characters)

Response (JSON)

JSON object containing search results

FieldTypeDescription
successbooleanAlways true on successful request
nodesarrayArray of matching nodes (max 10 items)
nodes[].addressstringFTN node address
nodes[].system_namestringNode system name
nodes[].sysop_namestringSysop name
nodes[].locationstringNode location
nodes[].domainstringNetwork domain

Error Responses

StatusDescription
401Authentication required

Notify

MethodPathAuthSummary
GET/api/notify/stateYesRetrieve current notification state for authenticated user.
POST/api/notify/stateYesUpdate notification state for authenticated user.
POST/api/notify/seenYesMark notification target as seen up to a given count.

GET /api/notify/state

Requires authentication

Returns the user's stored notification tracking state including mail counts, unread flags, and last-seen IDs for chat, files, and approvals. Returns sensible defaults if no state has been saved yet.

Response (JSON)

Notification state object

FieldTypeDescription
stateobjectNotification state
state.mailLastCountsobjectLast-seen mail counts
state.mailLastCounts.netmailintegerLast-seen netmail count
state.mailLastCounts.echomailintegerLast-seen echomail max ID
state.mailUnreadobjectUnread mail flags
state.mailUnread.netmailbooleanWhether netmail has unread messages
state.mailUnread.echomailbooleanWhether echomail has unread messages
state.chatLastTotalintegerLast-seen chat message ID
state.chatUnreadbooleanWhether chat has unread messages
state.filesLastMaxIdintegerLast-seen file max ID
state.filesUnreadbooleanWhether there are new files

Error Responses

StatusDescription
400Unable to resolve user session

POST /api/notify/state

Requires authentication

Persists notification tracking state (mail counts, unread flags, last-seen IDs). Normalizes and validates all numeric values (non-negative integers) and boolean flags before storage.

Request Body (JSON)

Notification state to persist

FieldTypeRequiredDescription
stateobjectYesState object with mailLastCounts, mailUnread, chatLastTotal, chatUnread, filesLastMaxId, filesUnread

Response (JSON)

Update confirmation with normalized state

FieldTypeDescription
successbooleanWhether state was saved
stateobjectNormalized state as stored
state.mailLastCountsobjectLast-seen mail counts
state.mailLastCounts.netmailintegerLast-seen netmail count
state.mailLastCounts.echomailintegerLast-seen echomail max ID
state.mailUnreadobjectUnread mail flags
state.mailUnread.netmailbooleanWhether netmail has unread messages
state.mailUnread.echomailbooleanWhether echomail has unread messages
state.chatLastTotalintegerLast-seen chat message ID
state.chatUnreadbooleanWhether chat has unread messages
state.filesLastMaxIdintegerLast-seen file max ID
state.filesUnreadbooleanWhether there are new files

Error Responses

StatusDescription
400Invalid state payload or unable to resolve user session

POST /api/notify/seen

Requires authentication

Updates the user's last-seen marker for a specific notification target (netmail, echomail, chat, files, or file-approvals). File-approvals is admin-only. Stores either a count (netmail) or max row ID (others) depending on target type.

Request Body (JSON)

Seen notification data

FieldTypeRequiredDescription
targetstringYesNotification target: 'netmail', 'echomail', 'chat', 'files', or 'file-approvals'
current_countintegerYesCount (netmail) or max row ID (others) to mark as seen

Response (JSON)

Confirmation of seen marker update

FieldTypeDescription
successbooleanWhether marker was updated
targetstringTarget that was updated
countintegerCount/ID that was stored

Error Responses

StatusDescription
400Invalid target or unable to resolve user session
403Insufficient permissions (file-approvals requires admin)

Pending Users

MethodPathAuthSummary
GET/api/admin/pending-usersYesList all pending user registrations (admin only).
GET/api/admin/pending-users/historyYesSearch approved registration history (admin only).
GET/api/admin/pending-users/{id}YesRetrieve single pending user registration details.
POST/api/admin/pending-users/{id}/approveYesApprove pending user registration and create active account.
POST/api/admin/pending-users/{id}/rejectYesReject pending user registration.

GET /api/admin/pending-users

Requires authentication

Retrieves all users awaiting admin approval. Requires admin privileges. Returns array of pending user records with application details. Throws 500 on database errors.

Response (JSON)

List of pending user registrations

FieldTypeDescription
successbooleanTrue on success
usersarrayArray of pending user registration objects
users[].idintegerPending user record ID
users[].usernamestringRequested username
users[].emailstring|nullEmail address provided during registration
users[].real_namestring|nullReal name provided during registration
users[].reasonstring|nullReason for registration (if required)
users[].requested_atstringRegistration request timestamp (ISO 8601)
users[].ip_addressstring|nullIP address at time of registration
users[].statusstringCurrent status (pending, approved, rejected)
users[].reviewed_byinteger|nullUser ID of admin who reviewed
users[].reviewed_atstring|nullReview timestamp (ISO 8601)
users[].admin_notesstring|nullAdmin notes on the registration
users[].reviewed_by_usernamestring|nullUsername of reviewing admin
users[].registration_sourcestringRegistration source (web, terminal, etc.)

Error Responses

StatusDescription
403User is not an admin
401Authentication required
500Database error

GET /api/admin/pending-users/history

Requires authentication

Searches retained approved registration records for admin lookup. Returns the most recent approved registrations by default and can filter by a free-text search over requested username, real name, email, or the created account username.

Query Parameters

NameTypeRequiredDescription
searchstringNoFree-text filter applied to username, real name, email, and created account username
limitintegerNoMaximum records to return. Defaults to 50, capped at 100

Response (JSON)

Approved registration history results

FieldTypeDescription
successbooleanTrue on success
usersarrayArray of approved registration records
users[].idintegerPending user record ID
users[].usernamestringRequested username
users[].emailstring|nullEmail address provided during registration
users[].real_namestring|nullReal name provided during registration
users[].requested_atstringRegistration request timestamp (ISO 8601)
users[].reviewed_atstring|nullApproval timestamp (ISO 8601)
users[].reviewed_byinteger|nullUser ID of admin who approved
users[].reviewed_by_usernamestring|nullUsername of approving admin
users[].created_user_idinteger|nullUser ID created from this registration
users[].created_user_usernamestring|nullUsername of the created account
users[].created_user_real_namestring|nullReal name of the created account
users[].registration_sourcestringRegistration source (web, terminal, etc.)
users[].admin_notesstring|nullAdmin notes stored with the approval

Error Responses

StatusDescription
403User is not an admin
401Authentication required
500Database error

GET /api/admin/pending-users/{id}

Requires authentication

Fetches a specific pending user record by ID, including referrer information (username and real name). Admin-only endpoint. Returns 404 if pending user not found.

Path Parameters

NameTypeDescription
idintegerPending user ID

Response (JSON)

Pending user registration details

FieldTypeDescription
successbooleanTrue on success
userobjectPending user registration details
user.idintegerPending user record ID
user.usernamestringRequested username
user.emailstring|nullEmail address
user.real_namestring|nullReal name
user.reasonstring|nullRegistration reason
user.requested_atstringRegistration request timestamp (ISO 8601)
user.ip_addressstring|nullIP address at registration
user.statusstringCurrent status (pending, approved, rejected)
user.admin_notesstring|nullAdmin notes
user.reviewed_byinteger|nullUser ID of reviewing admin
user.reviewed_by_usernamestring|nullUsername of reviewing admin
user.reviewed_atstring|nullReview timestamp (ISO 8601)
user.referrer_idinteger|nullUser ID of the referrer
user.referrer_usernamestring|nullUsername of the referrer
user.referrer_real_namestring|nullReal name of the referrer
user.created_user_idinteger|nullUser ID created from this registration after approval
user.created_user_usernamestring|nullUsername of the created account
user.created_user_real_namestring|nullReal name of the created account
user.registration_sourcestringRegistration source (web, terminal, etc.)

Error Responses

StatusDescription
403User is not an admin
404Pending user not found
401Authentication required
500Database error

POST /api/admin/pending-users/{id}/approve

Requires authentication

Converts a pending user to an active user account. Admin-only. Accepts optional notes field. Returns newly created user ID on success. The original registration row is retained as an approved audit record linked to the created user account. Throws 400 if approval fails (e.g., duplicate username, invalid state).

Path Parameters

NameTypeDescription
idintegerPending user ID to approve

Request Body (JSON)

Approval details

FieldTypeRequiredDescription
notesstringNoOptional admin notes for approval

Response (JSON)

Approval confirmation with new user ID

FieldTypeDescription
successbooleanTrue if approved
new_user_idintegerID of newly created active user
message_codestringLocalization key for success message

Error Responses

StatusDescription
400Approval failed (invalid state, duplicate username, etc.)
403User is not an admin
401Authentication required

POST /api/admin/pending-users/{id}/reject

Requires authentication

Denies a pending user registration. Admin-only. Accepts optional notes field. The registration row is retained with status = rejected. Throws 400 if rejection fails.

Path Parameters

NameTypeDescription
idintegerPending user ID to reject

Request Body (JSON)

Rejection details

FieldTypeRequiredDescription
notesstringNoOptional admin notes for rejection

Response (JSON)

Rejection confirmation

FieldTypeDescription
successbooleanTrue if rejected
message_codestringLocalization key for success message

Error Responses

StatusDescription
400Rejection failed
403User is not an admin
401Authentication required

Polls

MethodPathAuthSummary
GET/api/polls/activeYesRetrieve all active polls with options and user vote status.
POST/api/polls/{id}/voteYesSubmit a vote for a specific poll option.
POST/api/polls/createYesCreate a new poll with question and multiple choice options.

GET /api/polls/active

Requires authentication

Fetches active polls from the database with their options and indicates which polls the authenticated user has already voted on. Returns an empty array if no active polls exist. Polls are ordered by creation date (newest first).

Response (JSON)

JSON object containing array of active polls

FieldTypeDescription
pollsarray of objectsArray of poll objects. Unvoted polls come first, then voted polls.
polls[].idintegerPoll ID
polls[].questionstringPoll question text
polls[].optionsarrayAnswer options
polls[].options[].idintegerOption ID
polls[].options[].option_textstringOption display text
polls[].has_votedbooleanWhether the authenticated user has voted on this poll
polls[].resultsarray(present only when has_voted is true) Per-option vote counts
polls[].results[].option_idintegerOption ID
polls[].results[].option_textstringOption display text
polls[].results[].votesintegerVote count for this option
polls[].total_votesinteger(present only when has_voted is true) Total votes cast

POST /api/polls/{id}/vote

Requires authentication

Records a vote for the authenticated user on an active poll. Validates that the poll exists and is active, and that the option belongs to the poll. Prevents duplicate votes via database constraint. Returns success or appropriate error with localized message.

Path Parameters

NameTypeDescription
idintegerThe poll ID to vote on

Request Body (JSON)

JSON object with poll option selection

FieldTypeRequiredDescription
option_idintegerYesThe poll option ID to vote for (must be > 0)

Response (JSON)

JSON object with success status

FieldTypeDescription
successbooleanTrue when vote is recorded

Error Responses

StatusDescription
400Missing or invalid option_id, invalid option for poll, or vote recording failed
404Poll not found or is not active

POST /api/polls/create

Requires authentication

Creates a new poll with validation for question length (10-500 chars) and option count (2-10 options, each max 200 chars). Prevents duplicate options. Requires authenticated user. Returns created poll details or validation error.

Request Body (JSON)

JSON object with poll details

FieldTypeRequiredDescription
questionstringYesPoll question (10-500 characters)
optionsarray of stringsYesArray of 2-10 poll options (each max 200 characters, no duplicates)

Response (JSON)

JSON object with created poll ID and details

FieldTypeDescription
idintegerThe newly created poll ID

Error Responses

StatusDescription
400Missing question, invalid question length, invalid option count, empty option, option too long, or duplicate options

Qwk

MethodPathAuthSummary
POST/api/qwk/uploadYesUpload and process a QWK REP packet for offline mail import.
GET/api/qwk/statusYesRetrieve user's QWK subscription status and pending message counts.
POST/api/qwk/formatYesSave user's preferred QWK packet format (QWK or QWKE).
POST/api/qwk/resetYesDev-only: reset all QWK state for the current user.
GET/api/qwk/area-selectionsYesRetrieve user's QWK area selections and available subscriptions.
POST/api/qwk/area-selectionsYesSave user's QWK area selection for packet generation.
GET/api/qwk/area-searchYesSearch echo areas by tag or description for QWK selection.

POST /api/qwk/upload

Requires authentication

Accepts a multipart form upload containing a REP packet (field name: "rep") and processes it for the authenticated user. Returns import statistics including message counts and any processing errors. QWK feature must be enabled on the system. Validates file type and handles various upload/processing failures with specific error codes.

Request Body (JSON)

Multipart form data with REP packet file

FieldTypeRequiredDescription
repfileYesREP packet file (QWK reply packet)

Response (JSON)

Import result with message statistics

FieldTypeDescription
successbooleanWhether processing completed successfully
importedintegerNumber of messages imported from the packet
skippedintegerNumber of messages skipped (duplicates, errors)
errorsarray of stringsError messages encountered during processing; empty array if none

Error Responses

StatusDescription
400No file uploaded, invalid file extension, or upload error
403QWK feature is disabled on this system
500REP packet processing failed

GET /api/qwk/status

Requires authentication

Returns the user's current QWK configuration including subscribed conferences and the number of new messages waiting since the last download. Respects custom area selections if enabled; otherwise uses all subscribed echoareas. Includes netmail status and per-area message counts.

Response (JSON)

QWK status with subscriptions and message counts

FieldTypeDescription
total_new_messagesintegerTotal new messages across all conferences
last_downloadstring|nullTimestamp of the last packet download (ISO 8601); null if never downloaded
conferencesarrayList of QWK conference objects
conferences[].numberintegerQWK conference number (0 = Personal Mail)
conferences[].namestringConference name (echoarea tag or 'Personal Mail')
conferences[].is_netmailbooleanWhether this conference is the personal netmail conference
conferences[].new_messagesintegerNumber of new messages in this conference
formatstringUser's preferred packet format ('qwk' or 'qwke')
limitintegerMaximum messages per packet (user-configurable)
hard_capintegerSystem-wide maximum messages per packet
is_devbooleanWhether the system is in development mode
has_custom_selectionbooleanWhether user has a custom area selection active

Error Responses

StatusDescription
403QWK feature is disabled

POST /api/qwk/format

Requires authentication

Persists the user's packet format preference to UserMeta. Accepts either 'qwk' (standard) or 'qwke' (extended) format. Used by the client to request the appropriate packet type on download.

Request Body (JSON)

Format preference

FieldTypeRequiredDescription
formatstringYesEither 'qwk' or 'qwke'

Response (JSON)

Confirmation of saved format

FieldTypeDescription
successbooleanAlways true on success
formatstringThe saved format value

Error Responses

StatusDescription
400Invalid format value (must be 'qwk' or 'qwke')
403QWK feature is disabled

POST /api/qwk/reset

Requires authentication

Purges all QWK-related database records (conference state, download log, message index, imported hashes) for the authenticated user, allowing packets to be re-downloaded from scratch. Only available when IS_DEV=true in environment configuration.

Response (JSON)

Reset confirmation

FieldTypeDescription
successbooleanTrue if reset completed
errorstringError message if reset failed

Error Responses

StatusDescription
403Not in dev mode (IS_DEV != 'true')
500Database operation failed

GET /api/qwk/area-selections

Requires authentication

Returns the user's current QWK area selection (if custom mode is active) plus the full list of areas they are subscribed to. Used by the UI to render the area picker. When custom selection is inactive, the selections array is empty (indicating all subscribed areas are used).

Response (JSON)

Area selection state and available areas

FieldTypeDescription
has_custombooleanTrue if user has an explicit custom selection active
selectionsarrayCurrently selected areas (empty if has_custom is false)
selections[].idintegerEcho area ID
selections[].tagstringEcho area tag
selections[].domainstringEcho area domain
selections[].descriptionstringEcho area description
subscribedarrayAll echo areas the user is subscribed to
subscribed[].idintegerEcho area ID
subscribed[].tagstringEcho area tag
subscribed[].domainstringEcho area domain
subscribed[].descriptionstringEcho area description

Error Responses

StatusDescription
403QWK feature is disabled

POST /api/qwk/area-selections

Requires authentication

Replaces the user's QWK area selection with the provided list of echoarea IDs. An empty array clears custom selection and reverts to using all subscribed areas. Validates that each area is active and accessible (respects sysop-only restrictions). Atomically updates the selection and toggles custom mode flag.

Request Body (JSON)

Area selection update

FieldTypeRequiredDescription
echoarea_idsarrayYesArray of echoarea IDs to select (empty array clears custom selection)
resetbooleanNoIf true, clears custom mode and reverts to all-subscribed behavior

Response (JSON)

Confirmation of saved selection

FieldTypeDescription
successbooleanTrue if selection was saved
countinteger|nullNumber of areas saved; null if reset=true

Error Responses

StatusDescription
400Missing or invalid echoarea_ids array
403QWK feature is disabled or user lacks access to specified areas

Requires authentication

Full-text search across active echoareas by tag or description. Returns up to 20 matching results. Respects sysop-only restrictions for non-admin users. Requires minimum 2-character search term. Used by the area picker UI to help users discover and add areas.

Query Parameters

NameTypeRequiredDescription
qstringYesSearch term (minimum 2 characters)

Response (JSON)

Search results

FieldTypeDescription
areasarrayMatching echo area objects (up to 20 results)
areas[].idintegerEcho area ID
areas[].tagstringEcho area tag
areas[].domainstringEcho area domain
areas[].descriptionstringEcho area description

Error Responses

StatusDescription
403QWK feature is disabled

Referrals

MethodPathAuthSummary
GET/api/referrals/my-statsYesGet authenticated user's referral statistics.
GET/api/referrals/admin/statsYesGet system-wide referral statistics (admin only).

GET /api/referrals/my-stats

Requires authentication

Returns the user's referral code, shareable referral URL, list of users they've referred, total referral count, earnings from referrals, and the per-referral bonus amount. Requires authentication and returns 404 if user has no referral code.

Response (JSON)

User's referral statistics and earnings

FieldTypeDescription
referral_codestringUnique referral code for this user
referral_urlstringFull URL for sharing referral link
referralsarrayList of users referred by this user
referrals[].usernamestringUsername of the referred user
referrals[].real_namestringReal name of the referred user
referrals[].created_atstringAccount creation timestamp (ISO 8601)
total_countintegerTotal number of users referred
total_earnedintegerTotal credits earned from referral bonuses
referral_bonusintegerCredits awarded per successful referral

Error Responses

StatusDescription
401Authentication required
404Referral code not found for user

GET /api/referrals/admin/stats

Requires authentication

Returns aggregated referral metrics including total referrals, top 10 referrers with counts, 10 most recent referrals, and total credits awarded system-wide. Requires admin authentication.

Response (JSON)

System-wide referral statistics

FieldTypeDescription
total_referralsintegerTotal number of users referred across system
top_referrersarrayTop 10 referrers
top_referrers[].usernamestringReferrer's username
top_referrers[].real_namestringReferrer's real name
top_referrers[].referral_countintegerNumber of successful referrals
recent_referralsarray10 most recent referral signups
recent_referrals[].usernamestringReferred user's username
recent_referrals[].created_atstringAccount creation timestamp (ISO 8601)
recent_referrals[].referrerstringUsername of the referrer
total_credits_awardedintegerTotal credits distributed as referral bonuses

Error Responses

StatusDescription
401Authentication required
403Admin privileges required

Register

MethodPathAuthSummary
POST/api/registerNoRegister a new user account with anti-spam protections.

POST /api/register

Public

Creates a new user account with built-in anti-spam validation (honeypot, timing checks). Supports both JSON and form-encoded requests. Terminal clients (telnet/SSH) can bypass browser-only anti-spam checks by providing a valid X-Binkterm-Registration-Token header. Accepts optional X-Binkterm-Registration-Source and X-Binkterm-Client-IP headers for terminal registrations. When registration approval is disabled and the account is auto-approved immediately, this endpoint also creates an authenticated session, returns a CSRF token, and sets the binktermphp_session cookie just like a normal login.

Request Body (JSON)

User registration data (JSON or form-encoded)

FieldTypeRequiredDescription
usernamestringYesDesired username
passwordstringYesAccount password
emailstringYesEmail address
websitestringNoHoneypot field—must be empty or request fails silently

Response (JSON)

Registration result with success status and optional auto-login details

FieldTypeDescription
successbooleanWhether registration succeeded
auto_approvedbooleanWhether the account was activated immediately
message_codestringLocalization key for success message
csrf_tokenstringnull

Error Responses

StatusDescription
400Invalid submission (honeypot triggered, too fast, missing fields, or validation failed)
429Rate limit exceeded
500Server error during registration

Shoutbox

MethodPathAuthSummary
GET/api/shoutboxYesRetrieve recent shoutbox messages with pagination.
POST/api/shoutboxYesPost a new message to the shoutbox.

GET /api/shoutbox

Requires authentication

Fetches non-hidden shoutbox messages ordered by creation date (newest first). Supports pagination via limit and offset query parameters. Limit is capped at 100 and defaults to 20. Includes username and timestamp for each message.

Query Parameters

NameTypeRequiredDescription
limitintegerNoNumber of messages to return (default 20, max 100)
offsetintegerNoNumber of messages to skip for pagination (default 0)

Response (JSON)

JSON object containing array of shoutbox messages

FieldTypeDescription
messagesarray of objectsArray of shoutbox message objects
messages[].idintegerMessage ID
messages[].messagestringMessage text
messages[].created_atstringISO 8601 creation timestamp
messages[].usernamestringUsername of the poster

POST /api/shoutbox

Requires authentication

Adds a new message to the shoutbox for the authenticated user. Validates message is not empty and does not exceed 280 characters. Returns success confirmation or validation error with localized message.

Request Body (JSON)

JSON object with shoutbox message

FieldTypeRequiredDescription
messagestringYesMessage text (1-280 characters)

Response (JSON)

JSON object with success status

FieldTypeDescription
successbooleanTrue when message is posted

Error Responses

StatusDescription
400Message is empty or exceeds 280 characters

Stream

MethodPathAuthSummary
GET/api/streamNoServer-Sent Events stream for real-time updates.
POST/api/streamYesExecute a real-time command (e.g., presence, notifications).

GET /api/stream

Public

Establishes a short-lived SSE connection that pushes events newer than the client's Last-Event-ID cursor. Connection closes after sending buffered events with a 'reconnect' event, allowing clients to reconnect without hammering the server. No authentication required; user context determined from session if available.

Response (JSON)

Server-Sent Events stream

FieldTypeDescription
idstringEvent ID (numeric cursor for reconnection)
eventstringEvent type (e.g., 'message', 'user_online', 'reconnect')
datastringJSON-encoded event payload

POST /api/stream

Requires authentication

Dispatches real-time commands to the CommandDispatcher for actions like updating presence, triggering notifications, or other real-time state changes. Command and payload structure depend on registered command handlers.

Request Body (JSON)

Real-time command

FieldTypeRequiredDescription
commandstringYesCommand name (case-insensitive)
payloadobjectYesCommand-specific payload

Response (JSON)

Command execution result

FieldTypeDescription
successbooleanWhether command executed successfully
*mixedCommand-specific response fields

Error Responses

StatusDescription
400Invalid payload (not JSON or missing command/payload)
400Unknown real-time command

Subscriptions

MethodPathAuthSummary
GET/api/subscriptions/userNoRetrieve user subscription information.
POST/api/subscriptions/userNoCreate or update user subscription.
GET/api/subscriptions/adminNoRetrieve admin subscription statistics.
POST/api/subscriptions/adminNoManage admin subscription settings.

GET /api/subscriptions/user

Requires authentication

Fetches all active echo areas with the authenticated user's subscription status for each.

Response (JSON)

Echo areas with per-user subscription state

FieldTypeDescription
echoareasarrayAll active echo areas visible to the user
echoareas[].idintegerEcho area ID
echoareas[].tagstringEcho area tag
echoareas[].descriptionstringHuman-readable description
echoareas[].domainstringDomain (e.g. "lovlynet")
echoareas[].is_localbooleanWhether the area is local-only
echoareas[].is_sysop_onlybooleanWhether the area is restricted to sysops
echoareas[].is_default_subscriptionbooleanWhether new users are auto-subscribed
echoareas[].is_newbooleanTrue if the area was created in the last 30 days
echoareas[].subscribedboolean|nullTrue if the user has an active subscription, null if never subscribed
echoareas[].subscription_typestring|null"user" (manually subscribed) or "auto" (default subscription), null if not subscribed
echoareas[].subscribed_atstring|nullISO 8601 timestamp when the user subscribed, null if not subscribed

POST /api/subscriptions/user

Requires authentication

Subscribe or unsubscribe the authenticated user from an echo area.

Request Body (JSON)

Subscription action

FieldTypeRequiredDescription
actionstringYesEither "subscribe" or "unsubscribe"
echoarea_idintegerYesEcho area to act on

Response (JSON)

Subscription action result

FieldTypeDescription
successbooleanTrue if the action was applied
message_codestringLocalization key for UI message (present on success; e.g. "ui.user_subscriptions.subscribed_success")

Error Responses

StatusDescription
400Missing echoarea_id or invalid action
401Authentication required

GET /api/subscriptions/admin

Requires authentication

Fetches all active echo areas with subscriber statistics and system-wide subscription totals. Requires admin privileges.

Response (JSON)

Echo area subscription statistics

FieldTypeDescription
echoareasarrayActive echo areas with subscriber counts
echoareas[].idintegerEcho area ID
echoareas[].tagstringEcho area tag
echoareas[].descriptionstringDescription
echoareas[].is_default_subscriptionbooleanWhether the area is a default subscription
echoareas[].subscriber_countintegerTotal active subscribers
echoareas[].user_subscribersintegerSubscribers with type "user" (manually subscribed)
echoareas[].auto_subscribersintegerSubscribers with type "auto" (default subscription)
statsobjectSystem-wide subscription totals
stats.total_echoareasintegerTotal active echo areas
stats.default_echoareasintegerEcho areas marked as default subscriptions
stats.total_subscriptionsintegerTotal active subscriptions across all users
stats.subscribed_usersintegerNumber of distinct users with at least one active subscription

Error Responses

StatusDescription
401Authentication required
403Admin access required

POST /api/subscriptions/admin

Requires authentication

Update administrative subscription settings for an echo area. Requires admin privileges.

Request Body (JSON)

Admin subscription action

FieldTypeRequiredDescription
actionstringYesCurrently supported: "set_default"
echoarea_idintegerYesEcho area to update
is_defaultbooleanNoRequired for set_default; true to mark as default, false to unmark

Response (JSON)

Admin action result

FieldTypeDescription
successbooleanTrue if the action was applied
message_codestringLocalization key for UI message (present on success; e.g. "ui.admin_subscriptions.default_enabled_success")

Error Responses

StatusDescription
400Missing echoarea_id or invalid action
401Authentication required
403Admin access required

System

MethodPathAuthSummary
GET/api/system/statusYesGet basic system status information.

GET /api/system/status

Requires authentication

Returns system-level statistics including message count for the current day. The last_poll field is not yet implemented. This endpoint provides minimal status info; more comprehensive monitoring may require additional endpoints.

Response (JSON)

System status metrics

FieldTypeDescription
last_pollnullLast BinkP poll timestamp (not yet implemented)
messages_todayintegerCount of echomail messages received today

Error Responses

StatusDescription
401Authentication required

Taglines

MethodPathAuthSummary
GET/api/taglinesYesRetrieve all configured BBS taglines.

GET /api/taglines

Requires authentication

Loads and returns all taglines from the BBS taglines configuration file. Taglines are parsed from newline-separated entries and empty lines are filtered out. Useful for displaying random taglines in the UI.

Response (JSON)

List of available taglines

FieldTypeDescription
successbooleanWhether taglines were successfully loaded
taglinesarray of stringsPlain text tagline strings, one per entry

Error Responses

StatusDescription
500Failed to load taglines

Test

MethodPathAuthSummary
GET/api/testNoSimple health check endpoint.

GET /api/test

Public

Returns a basic success response with current server timestamp. Useful for verifying API availability and connectivity.

Response (JSON)

Test success response with timestamp

FieldTypeDescription
teststringAlways 'success'
timestampstringCurrent server time (Y-m-d H:i:s format)

Url Preview

MethodPathAuthSummary
GET/api/url-previewYesFetch Open Graph metadata from a URL for preview unfurling.

GET /api/url-preview

Requires authentication

Retrieves Open Graph and meta tags from a given URL for rich preview display in the compose UI. Includes SSRF protection: blocks requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16). Follows redirects (max 5) with 8-second timeout. Validates URL format and protocol (http/https only).

Query Parameters

NameTypeRequiredDescription
urlstringYesURL to fetch preview for (must start with http:// or https://)

Response (JSON)

Open Graph metadata or error

FieldTypeDescription
successbooleanFetch status
titlestringog:title or page title
descriptionstringog:description or meta description
imagestringog:image URL
error_codestringError code if fetch failed

Error Responses

StatusDescription
400Invalid URL format, private IP range, or fetch timeout
401Authentication required

User

MethodPathAuthSummary
GET/api/user/echomail-ignore-rulesYesRetrieve all echomail ignore rules for the authenticated user.
DELETE/api/user/echomail-ignore-rules/{id}YesDelete an echomail ignore rule for the authenticated user.
GET/api/user/profileYesRetrieve the authenticated user's profile information.
GET/api/user/public-profile/{id}YesRetrieve public profile information for an active user by ID.
POST/api/user/change-passwordYesChange the authenticated user's password.
POST/api/user/profileYesUpdate the authenticated user's profile information.
GET/api/user/statsYesRetrieve message and file transfer statistics for the authenticated user.
GET/api/user/stats/{userId}YesRetrieve user statistics including message counts.
GET/api/user/transactions/{userId}YesRetrieve paginated transaction history for a user.
GET/api/user/activity/{userId}YesRetrieve paginated activity log for a user.
GET/api/user/creditsYesGet current user's credit balance.
GET/api/user/sessionsYesList all active sessions for authenticated user.
DELETE/api/user/sessions/{sessionId}YesRevoke a specific user session.
DELETE/api/user/sessions/allYesRevoke all sessions for authenticated user.
GET/api/user/echolist-preferenceYesRetrieve echolist filter preferences for authenticated user.
POST/api/user/echolist-preferenceYesUpdate echolist filter preferences for authenticated user.
POST/api/user/activityYesUpdate user's current activity status.
GET/api/user/sharesYesList all message shares created by the authenticated user.
GET/api/pgp/key/{userId}NoList public PGP keys for a user and return the preferred key first.
GET/api/user/pgp/keysYesList the authenticated user's saved PGP keys.
POST/api/user/pgp/keysYesUpload a public PGP key for the authenticated user.
POST/api/user/pgp/keys/managedYesStore a browser-generated managed PGP keypair for the authenticated user.
POST/api/user/pgp/keys/{fingerprint}/primaryYesSet the preferred public PGP key for the authenticated user.
DELETE/api/user/pgp/keys/{fingerprint}YesDelete one of the authenticated user's PGP keys.
GET/api/user/pgp/private-key/{fingerprint}YesFetch the encrypted private key blob for a managed PGP key.
GET/api/user/settingsYesRetrieve authenticated user's settings and preferences.
POST/api/user/settingsYesUpdate authenticated user's settings and preferences.
POST/api/user/reset-onboardingYesReset echomail onboarding flag for user.
GET/api/user/mcp-keyYesCheck MCP server key enrollment status.
POST/api/user/mcp-key/generateYesGenerate new MCP server authentication key.
DELETE/api/user/mcp-keyYesRevoke user's MCP server key.
GET/api/user/packetbbs-totp/statusYesCheck PacketBBS TOTP enrollment status.
POST/api/user/packetbbs-totp/setupYesGenerate a new pending TOTP secret for PacketBBS authenticator enrollment.
POST/api/user/packetbbs-totp/verify-enrollmentYesVerify TOTP code and activate the pending secret.
POST/api/user/packetbbs-totp/disableYesDisable and clear the user's PacketBBS TOTP secret.
GET/api/user/meshcore/contactsYesList the current user's registered MeshCore radio contacts.
POST/api/user/meshcore/contactsYesRegister a MeshCore radio contact for the current user.
PUT/api/user/meshcore/contacts/{id}YesUpdate a user's MeshCore contact name.
DELETE/api/user/meshcore/contacts/{id}YesDelete a user's MeshCore contact.
GET/api/user/terminal-settingsYesRetrieve user's terminal display settings.
POST/api/user/terminal-settingsYesUpdate user's terminal display settings.
GET/api/user/terminal-mail-stateYesRetrieve user's terminal mail navigation state.
POST/api/user/terminal-mail-stateYesUpdate user's terminal mail navigation state.
GET/api/user/web-mail-stateYesRetrieve web-specific mail pagination state for authenticated user.
POST/api/user/web-mail-stateYesUpdate web mail pagination state for authenticated user.

GET /api/user/echomail-ignore-rules

Requires authentication

Fetches the complete list of echomail ignore rules created by the authenticated user. Returns array of rule objects with sender name, address, and subject criteria.

Response (JSON)

User's echomail ignore rules

FieldTypeDescription
successbooleanOperation succeeded
rulesarrayArray of ignore rule objects with sender_name, sender_address, subject_contains

Error Responses

StatusDescription
401Authentication required

DELETE /api/user/echomail-ignore-rules/{id}

Requires authentication

Removes a specific ignore rule belonging to the authenticated user. The rule ID must be a positive integer. Returns 404 if the rule does not exist or does not belong to the user. Returns 400 if the rule ID is invalid.

Path Parameters

NameTypeDescription
idintegerThe ignore rule ID to delete

Response (JSON)

Confirmation of successful deletion

FieldTypeDescription
successbooleanAlways true on success
message_codestringLocalization key for the success message

Error Responses

StatusDescription
400Invalid rule ID (must be positive integer)
404Rule not found or does not belong to user

GET /api/user/profile

Requires authentication

Returns basic profile fields for the authenticated user: email, location, and about_me bio. All fields are strings and may be empty.

Response (JSON)

User profile data

FieldTypeDescription
successbooleanAlways true
profileobjectProfile object containing email, location, about_me
profile.emailstringUser email address
profile.locationstringUser location
profile.about_mestringUser bio/about section

GET /api/user/public-profile/{id}

Requires authentication

Returns a limited public profile for an active user. This endpoint is intended for authenticated client features such as the terminal Who's Online profile viewer and returns only public-facing fields rather than account management data.

Path Parameters

NameTypeDescription
idintegerUser ID of the active user whose public profile should be loaded

Response (JSON)

Public profile fields

FieldTypeDescription
successbooleanAlways true on success
profileobjectPublic profile data
profile.user_idintegerUser ID
profile.usernamestringUsername
profile.real_namestringFull/real name (may be empty)
profile.locationstringLocation (may be empty)
profile.about_mestringBiography/about-me text (may be empty)

Error Responses

StatusDescription
401Authentication required
404User not found

POST /api/user/change-password

Requires authentication

Updates the user's password after verifying the current password. New password must be at least 6 characters. Accepts JSON request body with old_password and new_password fields.

Request Body (JSON)

Password change request

FieldTypeRequiredDescription
old_passwordstringYesCurrent password for verification
new_passwordstringYesNew password (minimum 6 characters)

Response (JSON)

Success response with localization key

FieldTypeDescription
successbooleanPassword updated successfully
message_codestringLocalization key: 'ui.profile.updated_successfully'

Error Responses

StatusDescription
400Invalid input, current password incorrect, or new password too short
500Failed to update password

POST /api/user/profile

Requires authentication

Updates email, location, and about_me fields. Optionally changes password if current_password and new_password are provided. Real name cannot be changed. Accepts JSON or form-encoded input.

Request Body (JSON)

Profile update request

FieldTypeRequiredDescription
real_namestringNoReal name (read-only, ignored)
emailstringNoEmail address
locationstringNoUser location
about_mestringNoBio/about section
current_passwordstringNoCurrent password (required if changing password)
new_passwordstringNoNew password (minimum 6 characters)

Response (JSON)

Success response with updated real name and localization key

FieldTypeDescription
successbooleanProfile updated successfully
real_namestringUser's real name (unchanged)
message_codestringLocalization key: 'ui.profile.updated_successfully'

Error Responses

StatusDescription
400Current password incorrect, new password too short, or other validation error
500Failed to update profile

GET /api/user/stats

Requires authentication

Returns counts of netmail composed, echomail posted, and file downloads/uploads. Netmail count matches messages by username or real_name and local system addresses (includes pending/unspooled). Echomail count is user_id-based. File counts come from activity log (types 6=download, 7=upload).

Response (JSON)

User activity statistics

FieldTypeDescription
netmail_countintegerNumber of netmail messages composed by user
echomail_countintegerNumber of echomail messages posted by user
downloadsintegerNumber of file downloads
uploadsintegerNumber of file uploads

GET /api/user/stats/{userId}

Requires authentication

Fetches aggregated statistics for a specific user including netmail and echomail counts. Admin-only endpoint that verifies the target user exists and is active. Counts netmail by matching sender name (username or real name) and local system addresses to include pending/unspooled messages.

Path Parameters

NameTypeDescription
userIdintegerID of the user to retrieve statistics for

Response (JSON)

User statistics object with message counts

FieldTypeDescription
netmail_countintegerTotal netmail messages composed by user
echomail_countintegerTotal echomail messages composed by user

Error Responses

StatusDescription
403Admin privileges required
404User not found or inactive

GET /api/user/transactions/{userId}

Requires authentication

Returns transaction records for a specific user with pagination support. Admin-only endpoint. Transactions are ordered by creation date descending. Limit is capped at 50 records per request.

Path Parameters

NameTypeDescription
userIdintegerID of the user to retrieve transactions for

Query Parameters

NameTypeRequiredDescription
offsetintegerNoNumber of records to skip (default: 0)
limitintegerNoNumber of records to return, max 50 (default: 10)

Response (JSON)

Paginated transaction list

FieldTypeDescription
successbooleanOperation success flag
transactionsarrayArray of transaction objects
transactions[].idintegerTransaction ID
transactions[].user_idintegerUser who owns this transaction
transactions[].other_party_idinteger|nullOther party user ID (for transfers)
transactions[].amountintegerCredit amount (positive = credit, negative = debit)
transactions[].balance_afterintegerBalance after this transaction
transactions[].descriptionstringHuman-readable description
transactions[].transaction_typestringTransaction type code
transactions[].created_atstringISO 8601 creation timestamp
offsetintegerCurrent offset used in query
limitintegerCurrent limit used in query

Error Responses

StatusDescription
403Admin privileges required
404User not found or inactive

GET /api/user/activity/{userId}

Requires authentication

Returns user activity log entries with category and activity type information. Admin-only endpoint. Activities are ordered by creation date descending. Limit is capped at 100 records per request.

Path Parameters

NameTypeDescription
userIdintegerID of the user to retrieve activity log for

Query Parameters

NameTypeRequiredDescription
offsetintegerNoNumber of records to skip (default: 0)
limitintegerNoNumber of records to return, max 100 (default: 25)

Response (JSON)

Paginated activity log entries

FieldTypeDescription
successbooleanOperation success flag
activityarrayArray of activity log entries
activity[].idintegerActivity log entry ID
activity[].created_atstringISO 8601 timestamp of activity
activity[].categorystringActivity category name
activity[].activitystringActivity type label
activity[].object_namestring|nullName of the object involved in the activity
activity[].metaobject|nullAdditional metadata (structure varies by activity type)
offsetintegerCurrent offset used in query
limitintegerCurrent limit used in query

Error Responses

StatusDescription
403Admin privileges required
404User not found or inactive

GET /api/user/credits

Requires authentication

Returns the authenticated user's current credit balance and basic user information. No admin privileges required.

Response (JSON)

User credit information

FieldTypeDescription
idintegerUser ID
usernamestringUsername
credit_balanceintegerCurrent credit balance

GET /api/user/sessions

Requires authentication

Returns all non-expired sessions for the authenticated user with IP addresses and creation timestamps. Marks the current session. Useful for session management and security monitoring.

Response (JSON)

List of active sessions

FieldTypeDescription
sessionsarrayArray of active session objects
sessions[].idstringSession token ID
sessions[].ip_addressstring|nullIP address the session was created from
sessions[].created_atstringSession creation timestamp (ISO 8601)
sessions[].expires_atstringSession expiry timestamp (ISO 8601)
sessions[].is_currentinteger1 if this is the currently active session, 0 otherwise

DELETE /api/user/sessions/{sessionId}

Requires authentication

Deletes a single session belonging to the authenticated user. Users can only revoke their own sessions. Returns success message on deletion or 404 if session not found.

Path Parameters

NameTypeDescription
sessionIdstringSession ID to revoke

Response (JSON)

Revocation confirmation

FieldTypeDescription
successbooleanRevocation success flag
message_codestringLocalization key for success message

Error Responses

StatusDescription
404Session not found or does not belong to user

DELETE /api/user/sessions/all

Requires authentication

Deletes all sessions for the authenticated user and clears the session cookie, effectively logging out from all devices. Returns success message or 500 on failure.

Response (JSON)

Logout confirmation

FieldTypeDescription
successbooleanLogout success flag
message_codestringLocalization key for success message

Error Responses

StatusDescription
500Failed to revoke sessions

GET /api/user/echolist-preference

Requires authentication

Returns the user's echolist display preferences including whether to show only subscribed echoes and/or only unread messages. Preferences are stored per-user in the user_settings table and default to false if not previously set.

Response (JSON)

User's echolist filter preferences

FieldTypeDescription
subscribed_onlybooleanIf true, display only subscribed echoes
unread_onlybooleanIf true, display only echoes with unread messages

Error Responses

StatusDescription
401Authentication required

POST /api/user/echolist-preference

Requires authentication

Sets the user's echolist display preferences. Uses upsert logic to create or update the user_settings record. Boolean values are coerced from the input (any truthy value enables the filter).

Request Body (JSON)

Echolist filter preferences to set

FieldTypeRequiredDescription
subscribed_onlybooleanNoShow only subscribed echoes
unread_onlybooleanNoShow only echoes with unread messages

Response (JSON)

Confirmation of preference update

FieldTypeDescription
successbooleanAlways true on successful update

Error Responses

StatusDescription
401Authentication required

POST /api/user/activity

Requires authentication

Records the user's current activity in their active session. Requires a valid session cookie. The activity string is stored and visible to admins in the whosonline endpoint.

Request Body (JSON)

Activity status to record

FieldTypeRequiredDescription
activitystringNoDescription of current activity (e.g., 'Reading messages', 'Composing reply')

Response (JSON)

Confirmation of activity update

FieldTypeDescription
successbooleanTrue if activity was recorded

Error Responses

StatusDescription
400No active session found (missing session cookie)
401Authentication required

GET /api/user/shares

Requires authentication

Retrieves all active message shares owned by the authenticated user, including share keys, slugs, and metadata. Useful for managing and tracking shared messages.

Response (JSON)

User's message shares

FieldTypeDescription
successbooleanWhether the shares were successfully retrieved
sharesarrayArray of message share objects
shares[].idintegerShare record ID
shares[].message_idintegerID of the shared message
shares[].message_typestringMessage type ('echomail' or 'netmail')
shares[].message_subjectstring|nullSubject of the shared message
shares[].area_tagstringEcho area tag (or 'netmail' for netmail shares)
shares[].share_keystringShare token string
shares[].share_urlstringFull URL of the share link
shares[].created_atstringShare creation timestamp (ISO 8601)
shares[].expires_atstring|nullShare expiry timestamp; null for no expiry
shares[].is_publicbooleanWhether share is publicly accessible
shares[].access_countintegerNumber of times the share has been accessed
shares[].last_accessed_atstring|nullLast access timestamp (ISO 8601)

Error Responses

StatusDescription
500Failed to load user shares

GET /api/pgp/key/{userId}

Returns the public PGP keys associated with the specified user account. The first key in the response is the preferred key if one is set.

Response (JSON)

Public PGP key listing for one user.

FieldTypeDescription
successbooleanOperation success flag
preferred_keyobject|nullPreferred public key for the user, or null when no keys are saved
preferred_key.fingerprintstring40-character uppercase PGP fingerprint
preferred_key.armored_public_keystringASCII-armored public key block
preferred_key.sourcestringuploaded or managed
preferred_key.labelstring|nullOptional user-defined label
preferred_key.user_id_stringstring|nullParsed OpenPGP user ID string
preferred_key.emailstring|nullParsed email address from the key, if present
preferred_key.key_algorithmstring|nullParsed public key algorithm
preferred_key.key_created_atstring|nullKey creation timestamp in UTC when available
preferred_key.is_primarybooleanWhether this key is marked preferred
preferred_key.created_atstringServer-side record creation timestamp
keysarrayAll public keys for the user in preferred-first order
keys[].fingerprintstring40-character uppercase PGP fingerprint
keys[].armored_public_keystringASCII-armored public key block
keys[].sourcestringuploaded or managed
keys[].labelstring|nullOptional user-defined label
keys[].user_id_stringstring|nullParsed OpenPGP user ID string
keys[].emailstring|nullParsed email address from the key, if present
keys[].key_algorithmstring|nullParsed public key algorithm
keys[].key_created_atstring|nullKey creation timestamp in UTC when available
keys[].is_primarybooleanWhether this key is marked preferred
keys[].created_atstringServer-side record creation timestamp

Error Responses

StatusDescription
500Failed to load PGP keys

GET /api/pgp/lookup

Requires authentication

Performs destination-aware public-key lookup for the compose UI. Local destinations query this BBS's public-key store. Remote FTN destinations first check the authenticated user's saved correspondent keys, then resolve the node's BinkP host from the nodelist, prefer _hkps._tcp SRV records when available, and otherwise fall back to https://<nodelist-hostname>/pks/lookup.

Query Parameters

FieldTypeRequiredDescription
searchstringYesKey fingerprint or search text
addressstringNoDestination FTN address; blank means local delivery
opstringNoindex (default) for a candidate list or get for one armored key
modestringNocompose (default) for destination-aware compose lookup, or verify for message-signature verification that checks saved correspondent keys and never performs remote HKPS lookup

Response (JSON, op=index)

FieldTypeDescription
successbooleanOperation success flag
is_local_addressbooleanWhether the destination was treated as local
keysarrayMatching public keys
keys[].fingerprintstring40-character uppercase PGP fingerprint
keys[].user_id_stringstring|nullParsed OpenPGP user ID string or HKP uid line
keys[].usernamestring|nullLocal BBS username when the match came from the local store
keys[].key_algorithmstring|nullPublic key algorithm when known
keys[].key_created_atstring|nullKey creation timestamp when known
keys[].lookup_sourcestringlocal, saved_contact, remote_srv, or remote_host
keys[].address_book_entry_idinteger|nullLinked address-book entry ID when the match came from a saved correspondent key
keys[].address_book_namestring|nullLinked address-book contact name when the match came from a saved correspondent key
keys[].address_book_node_addressstring|nullLinked address-book FTN address when the match came from a saved correspondent key

Response (JSON, op=get)

FieldTypeDescription
successbooleanOperation success flag
is_local_addressbooleanWhether the destination was treated as local
keyobject|nullResolved public key, or null when no match was found
key.fingerprintstring40-character uppercase PGP fingerprint
key.armored_public_keystringASCII-armored public key block
key.user_id_stringstring|nullParsed OpenPGP user ID string
key.emailstring|nullParsed email address from the key, if available
key.key_algorithmstring|nullParsed public key algorithm
key.key_created_atstring|nullKey creation timestamp when known
key.lookup_sourcestringlocal, saved_contact, remote_srv, or remote_host
key.address_book_entry_idinteger|nullLinked address-book entry ID when the match came from a saved correspondent key
key.address_book_namestring|nullLinked address-book contact name when the match came from a saved correspondent key
key.address_book_node_addressstring|nullLinked address-book FTN address when the match came from a saved correspondent key

Error Responses

StatusDescription
500Failed to load PGP keys

GET /api/user/pgp/keys

Requires authentication

Lists the authenticated user's saved PGP keys, including whether each key has an encrypted managed private key blob stored on the server.

Response (JSON)

Authenticated user's PGP key inventory.

FieldTypeDescription
successbooleanOperation success flag
keysarraySaved PGP keys in preferred-first order
keys[].idintegerDatabase ID for the saved key row
keys[].fingerprintstring40-character uppercase PGP fingerprint
keys[].sourcestringuploaded or managed
keys[].labelstring|nullOptional user-defined label
keys[].user_id_stringstring|nullParsed OpenPGP user ID string
keys[].emailstring|nullParsed email address from the key, if present
keys[].key_algorithmstring|nullParsed public key algorithm
keys[].key_created_atstring|nullKey creation timestamp in UTC when available
keys[].is_primarybooleanWhether this key is marked preferred
keys[].created_atstringServer-side record creation timestamp
keys[].updated_atstringLast modification timestamp
keys[].has_private_keybooleanWhether an encrypted managed private key blob exists for this key

Error Responses

StatusDescription
500Failed to load PGP keys

POST /api/user/pgp/keys

Requires authentication

Uploads an ASCII-armored public key, parses its metadata, and stores it in the authenticated user's key inventory.

Request Body (JSON)

Public key upload payload.

FieldTypeRequiredDescription
armored_public_keystringYesASCII-armored public key block
labelstringNoOptional label shown in settings and key listings

Response (JSON)

Stored public key record.

FieldTypeDescription
successbooleanOperation success flag
message_codestringTranslation key for the success message
keyobjectStored key metadata
key.idintegerDatabase ID for the key row
key.fingerprintstring40-character uppercase PGP fingerprint
key.sourcestringuploaded
key.labelstring|nullOptional label shown in settings and key listings
key.user_id_stringstring|nullParsed OpenPGP user ID string
key.emailstring|nullParsed email address from the key, if present
key.key_algorithmstring|nullParsed public key algorithm
key.key_created_atstring|nullKey creation timestamp in UTC when available
key.is_primarybooleanWhether this key became the preferred key
key.created_atstringServer-side record creation timestamp
key.updated_atstringLast modification timestamp

Error Responses

StatusDescription
400Public key missing or invalid
500Failed to save PGP key

POST /api/user/pgp/keys/managed

Requires authentication

Stores a browser-generated managed PGP keypair. The server stores the ASCII-armored public key and the encrypted private key blob; it does not expose the private key blob except through the authenticated private-key endpoint.

Request Body (JSON)

Managed keypair storage payload.

FieldTypeRequiredDescription
armored_public_keystringYesASCII-armored public key block
encrypted_private_keystringYesASCII-armored encrypted private key block
labelstringNoOptional label shown in settings and key listings

Response (JSON)

Stored managed public key record.

FieldTypeDescription
successbooleanOperation success flag
message_codestringTranslation key for the success message
keyobjectStored public key metadata
key.idintegerDatabase ID for the key row
key.fingerprintstring40-character uppercase PGP fingerprint
key.sourcestringmanaged
key.labelstring|nullOptional label shown in settings and key listings
key.user_id_stringstring|nullParsed OpenPGP user ID string
key.emailstring|nullParsed email address from the key, if present
key.key_algorithmstring|nullParsed public key algorithm
key.key_created_atstring|nullKey creation timestamp in UTC when available
key.is_primarybooleanWhether this key became the preferred key
key.created_atstringServer-side record creation timestamp
key.updated_atstringLast modification timestamp

Error Responses

StatusDescription
400Public/private keypair missing or invalid
500Failed to save PGP key

POST /api/user/pgp/keys/{fingerprint}/primary

Requires authentication

Marks one saved PGP key as the preferred public key for the authenticated user.

Response (JSON)

Preference update confirmation.

FieldTypeDescription
successbooleanOperation success flag
message_codestringTranslation key for the success message

Error Responses

StatusDescription
404PGP key not found for this user
500Failed to save PGP key

DELETE /api/user/pgp/keys/{fingerprint}

Requires authentication

Deletes one saved PGP key from the authenticated user's inventory. If the deleted key was preferred, the oldest remaining key is promoted automatically.

Response (JSON)

Deletion confirmation.

FieldTypeDescription
successbooleanOperation success flag
message_codestringTranslation key for the success message

Error Responses

StatusDescription
404PGP key not found for this user
500Failed to delete PGP key

GET /api/user/pgp/private-key/{fingerprint}

Requires authentication

Fetches the encrypted private key blob for one managed PGP key owned by the authenticated user.

Response (JSON)

Encrypted private key record.

FieldTypeDescription
successbooleanOperation success flag
fingerprintstring40-character uppercase PGP fingerprint
encrypted_private_keystringASCII-armored encrypted private key block

Error Responses

StatusDescription
404Managed private key not found
500Failed to load PGP keys

GET /api/user/settings

Requires authentication

Fetches user settings including locale, shell preference, notification sounds, and composition options. Resolves and persists locale based on user preferences. Returns license validity status. Settings are merged from both user_settings table and UserMeta storage.

Response (JSON)

User settings object with locale, shell, notification preferences, and license status.

FieldTypeDescription
successbooleanOperation success flag
settingsobjectUser settings
settings.localestringUI locale code (e.g., 'en', 'fr')
settings.timezonestringUser's timezone (e.g., 'America/Los_Angeles')
settings.themestringUI theme (e.g., 'light', 'dark', 'amber')
settings.messages_per_pageintegerNumber of messages shown per page
settings.threaded_viewbooleanWhether echomail is shown in threaded mode
settings.netmail_threaded_viewbooleanWhether netmail is shown in threaded mode
settings.default_sortstringDefault sort order (date_desc, date_asc, subject, author)
settings.font_familystringUI font family
settings.font_sizeintegerUI font size in pixels
settings.date_formatstringDate format locale code (e.g., 'en-US')
settings.quote_coloringbooleanWhether quoted text is colorized
settings.default_echo_liststringDefault echo list view (reader, list)
settings.signature_textstring|nullUser's message signature
settings.default_taglinestring|nullDefault message tagline
settings.shellstringUI shell preference ('web' or 'bbs-menu')
settings.chat_notification_soundstringChat notification sound (disabled, notify1–5)
settings.echomail_notification_soundstringEchomail notification sound (disabled, notify1–5)
settings.netmail_notification_soundstringNetmail notification sound (disabled, notify1–5)
settings.file_notification_soundstringFile notification sound (disabled, notify1–5)
settings.compose_advanced_openbooleanWhether advanced compose panel is open by default
settings.compose_hard_wrapintegerHard-wrap column for message composition (0 = disabled)
settings.media_render_modestringMedia rendering mode ('click', 'auto')
settings.license_validbooleanWhether the system has a valid license

Error Responses

StatusDescription
500Failed to load user settings

POST /api/user/settings

Requires authentication

Updates user settings including locale, shell preference, and notification sounds. Validates notification sound values against allowed set (disabled, notify1-5). Shell changes respect AppearanceConfig lock. Locale changes are persisted. Composition settings (hard wrap, advanced mode) are stored in UserMeta.

Request Body (JSON)

Settings update payload

FieldTypeRequiredDescription
settingsobjectYesObject containing settings to update: locale, shell, chat_notification_sound, echomail_notification_sound, netmail_notification_sound, file_notification_sound, compose_advanced_open, compose_hard_wrap, media_render_mode

Response (JSON)

Confirmation of successful settings update.

FieldTypeDescription
successbooleanOperation success flag

Error Responses

StatusDescription
400Invalid input or missing settings object
500Failed to update user settings

POST /api/user/reset-onboarding

Requires authentication

Clears the interests_onboarded flag in UserMeta, allowing the user to be guided through the echomail onboarding process again.

Response (JSON)

Confirmation of successful reset.

FieldTypeDescription
successbooleanOperation success flag

Error Responses

StatusDescription
500Failed to reset onboarding flag

GET /api/user/mcp-key

Requires authentication

Returns whether the user has an MCP server key enrolled. Shows only a preview (first 8 chars + asterisks) if key exists. Requires MCP_SERVER_URL environment variable and valid license.

Response (JSON)

MCP key enrollment status.

FieldTypeDescription
successbooleanOperation success flag
has_keybooleanWhether user has an MCP key enrolled
key_previewstringFirst 8 characters of key followed by asterisks (only if has_key is true)

Error Responses

StatusDescription
403MCP services not enabled or valid license required

POST /api/user/mcp-key/generate

Requires authentication

Creates a new 64-character hex-encoded MCP server key and stores it in UserMeta. Returns the full key only at generation time; subsequent retrievals show preview only. Requires MCP_SERVER_URL and valid license.

Response (JSON)

Newly generated MCP server key.

FieldTypeDescription
successbooleanOperation success flag
keystringFull 64-character hex-encoded MCP server key

Error Responses

StatusDescription
403MCP services not enabled or valid license required
500Failed to generate MCP key

DELETE /api/user/mcp-key

Requires authentication

Deletes the user's MCP server key by setting it to null in UserMeta. Requires MCP_SERVER_URL and valid license.

Response (JSON)

Confirmation of successful key revocation.

FieldTypeDescription
successbooleanOperation success flag

Error Responses

StatusDescription
403MCP services not enabled or valid license required
500Failed to revoke MCP key

GET /api/user/packetbbs-totp/status

Requires authentication

Returns whether the user has PacketBBS TOTP (time-based one-time password) authentication enabled. Status is stored in UserMeta.

Response (JSON)

PacketBBS TOTP enrollment status.

FieldTypeDescription
successbooleanOperation success flag
enabledbooleanWhether PacketBBS TOTP is enabled for user

POST /api/user/packetbbs-totp/setup

Requires authentication

Initiates TOTP setup by generating a new secret and storing it as pending in user metadata. Returns the BASE32 secret, otpauth URI, and QR code SVG for client-side display. The secret is not activated until verified via /verify-enrollment. Useful for setting up time-based one-time password authentication.

Response (JSON)

TOTP setup data including secret and QR code

FieldTypeDescription
successbooleanAlways true on success
secretstringBASE32-encoded TOTP secret
uristringotpauth:// URI for authenticator apps
qr_codestringQR code as data:image/svg+xml;base64 URI

Error Responses

StatusDescription
500Setup failed (e.g., metadata write error)

POST /api/user/packetbbs-totp/verify-enrollment

Requires authentication

Validates a 6-digit code against the pending TOTP secret. On success, promotes the pending secret to active, sets enrollment state to enabled, and clears the pending secret. Code must be exactly 6 digits. Fails if no pending secret exists or code is invalid.

Request Body (JSON)

TOTP code for verification

FieldTypeRequiredDescription
codestringYes6-digit code from authenticator app

Response (JSON)

Confirmation of successful enrollment

FieldTypeDescription
successbooleanTrue if verification succeeded

Error Responses

StatusDescription
400Invalid code format (not 6 digits) or code verification failed
400No pending secret found; setup must be initiated first
500Failed to activate secret (metadata write error)

POST /api/user/packetbbs-totp/disable

Requires authentication

Removes all TOTP-related metadata for the authenticated user, including active secret, pending secret, and enabled flag. Effectively disables two-factor authentication via TOTP for the user.

Response (JSON)

Confirmation of successful disabling

FieldTypeDescription
successbooleanTrue if disabling succeeded

Error Responses

StatusDescription
500Failed to disable authenticator (metadata write error)

GET /api/user/meshcore/contacts

Requires authentication

Returns all MeshCore radio contacts registered by the current user.

Response (JSON)

FieldTypeDescription
successbooleanOperation success flag
contactsarrayList of contact objects
contacts[].idintegerContact record ID
contacts[].pub_key_prefixstring12-char hex node ID prefix
contacts[].pub_key_fullstring|nullFull 64-char public key, if known
contacts[].namestring|nullDisplay name
contacts[].adv_typestring|nullAdvertisement type reported by the radio
contacts[].last_seen_atstring|nullISO 8601 timestamp of last bridge contact

POST /api/user/meshcore/contacts

Requires authentication

Registers a MeshCore radio contact for the current user. Accepts either a 12-character node ID prefix or a full 64-character public key hex string.

Request Body (JSON)

FieldTypeRequiredDescription
node_idstringYes12-char or 64-char lowercase hex node ID
namestringNoOptional display name

Response (JSON)

FieldTypeDescription
successbooleanOperation success flag
idintegerNewly created contact record ID

Error Responses

StatusDescription
400errors.meshcore.invalid_node_id — node_id must be 12 or 64 lowercase hex chars
409errors.meshcore.contact_exists — a contact with this key already exists for the user

PUT /api/user/meshcore/contacts/{id}

Requires authentication

Updates the display name of a MeshCore contact owned by the current user.

Request Body (JSON)

FieldTypeRequiredDescription
namestringNoNew display name (empty string clears the name)

Response (JSON)

FieldTypeDescription
successbooleanOperation success flag

Error Responses

StatusDescription
404errors.meshcore.not_found — contact not found or not owned by this user

DELETE /api/user/meshcore/contacts/{id}

Requires authentication

Deletes a MeshCore contact owned by the current user.

Response (JSON)

FieldTypeDescription
successbooleanOperation success flag

Error Responses

StatusDescription
404errors.meshcore.not_found — contact not found or not owned by this user

GET /api/user/terminal-settings

Requires authentication

Fetches terminal configuration preferences for the authenticated user, including character set and ANSI color support. Returns current values from user metadata.

Response (JSON)

User terminal settings

FieldTypeDescription
successbooleanAlways true
settingsobjectTerminal configuration settings
settings.terminal_charsetstring|nullActive character set: utf8, cp437, or ascii; null if not set
settings.terminal_ansi_colorstring|nullANSI color mode: yes or no; null if not set
settings.term_shell_modestring|nullTerminal shell mode (e.g. auto or a configured shell name); null if not set

POST /api/user/terminal-settings

Requires authentication

Updates terminal configuration preferences for the authenticated user. Accepts both wrapped (settings object) and flat request formats. Validates values against allowed options: terminal_charset (utf8, cp437, ascii) and terminal_ansi_color (yes, no).

Request Body (JSON)

Terminal settings to update (wrapped or flat)

FieldTypeRequiredDescription
settingsobjectNoWrapped settings object (alternative to flat format)
terminal_charsetstringNoCharacter set: utf8, cp437, or ascii
terminal_ansi_colorstringNoANSI color support: yes or no

Response (JSON)

Confirmation of update

FieldTypeDescription
successbooleanTrue if update succeeded

Error Responses

StatusDescription
400Invalid value for terminal_charset or terminal_ansi_color

GET /api/user/terminal-mail-state

Requires authentication

Fetches saved terminal navigation state for the authenticated user, including mail-reader positions, the saved netmail and echomail sort selections, and the last selected local chat target. Used to restore UI state across sessions.

Response (JSON)

User mail navigation state

FieldTypeDescription
successbooleanAlways true
settingsobjectSaved terminal navigation state
settings.terminal_netmail_pageinteger|nullLast viewed netmail page number, or null if not set
settings.terminal_netmail_selected_message_idinteger|nullID of the last selected netmail message, or null if not set
settings.terminal_netmail_folderstring|nullLast viewed netmail folder (inbox or sent), or null if not set
settings.terminal_netmail_sortstring|nullLast used netmail sort order (date_desc, date_asc, subject, author), or null if not set
settings.terminal_echomail_areas_pageinteger|nullLast viewed echomail areas page number, or null if not set
settings.terminal_echomail_positionsobject|string|nullPer-area read position map (JSON object or string), or null if not set
settings.terminal_echomail_sortstring|nullLast used echomail sort order (date_desc, date_asc, subject, author), or null if not set
settings.terminal_chat_targetobject|string|nullLast selected terminal chat target (JSON object or string), or null if not set

POST /api/user/terminal-mail-state

Requires authentication

Persists terminal navigation state for the authenticated user, including mail-reader positions, the saved netmail and echomail sort selections, and the last selected local chat target. Accepts both wrapped and flat request formats. Integer fields must be positive or null; terminal_echomail_positions and terminal_chat_target accept JSON string or object.

Request Body (JSON)

Mail state to update (wrapped or flat)

FieldTypeRequiredDescription
settingsobjectNoWrapped settings object (alternative to flat format)
terminal_netmail_pageintegerNoCurrent netmail page (≥1 or null)
terminal_netmail_selected_message_idintegerNoSelected netmail message ID (≥1 or null)
terminal_netmail_folderstringNoSaved netmail folder: inbox or sent
terminal_netmail_sortstringNoSaved netmail list sort: date_desc, date_asc, subject, or author
terminal_echomail_areas_pageintegerNoCurrent echomail areas page (≥1 or null)
terminal_echomail_positionsobjectstringNo
terminal_echomail_sortstringNoSaved echomail list sort: date_desc, date_asc, subject, or author
terminal_chat_targetobjectstringNo

When terminal_chat_target is present it must include:

FieldTypeDescription
typestringroom or dm
idintegerTarget room ID or DM user ID
labelstringDisplay label used when restoring the target

Response (JSON)

Confirmation of update

FieldTypeDescription
successbooleanTrue if update succeeded

Error Responses

StatusDescription
400Invalid value for integer field (not numeric or < 1), invalid terminal_echomail_sort, or invalid terminal_echomail_positions/terminal_chat_target JSON

GET /api/user/web-mail-state

Requires authentication

Fetches user metadata for web interface mail positions, including netmail page number and per-area echomail page positions. This state is separate from telnet-based navigation and persists user's browsing position across web sessions. Returns null values if not previously set.

Response (JSON)

User's web mail state settings

FieldTypeDescription
successbooleanAlways true on success
settingsobjectMail state object containing web_netmail_page and web_echomail_positions
settings.web_netmail_pagestringnull
settings.web_echomail_positionsstringnull

Error Responses

StatusDescription
401Authentication required

POST /api/user/web-mail-state

Requires authentication

Persists user's web interface mail positions. Validates web_netmail_page as positive integer and web_echomail_positions as JSON object with area tags (max 128 chars) mapping to page numbers (minimum 1). Null values clear stored state. Rejects invalid formats with 400 error.

Request Body (JSON)

Mail state update payload

FieldTypeRequiredDescription
settingsobjectNoObject containing web_netmail_page and/or web_echomail_positions to update
web_netmail_pageintegernullNo
web_echomail_positionsobjectstringNo

Response (JSON)

Confirmation of state update

FieldTypeDescription
successbooleanTrue if update succeeded

Error Responses

StatusDescription
400Invalid web_netmail_page (non-numeric or <1) or malformed web_echomail_positions JSON
401Authentication required

Users

MethodPathAuthSummary
GET/api/admin/usersYesList all active users with pagination and search.
GET/api/admin/users/{id}YesRetrieve single user details for admin editing.
POST/api/admin/users/{id}/creditsYesGrant credits to a user account
POST/api/admin/users/{id}YesUpdate user account details
POST/api/admin/users/{id}/toggle-statusYesToggle user active/inactive status
POST/api/admin/users/createYesCreate a new user account
POST/api/admin/users/cleanupYesClean up old pending registrations
POST/api/admin/users/{userId}/send-reminderYesSend account reminder to a user
GET/api/admin/users/need-remindersYesList users eligible for account reminders

GET /api/admin/users

Requires authentication

Retrieves paginated list of active user accounts. Admin-only. Supports full-text search by username/email and configurable page size (max 100). Default limit is 25 per page.

Query Parameters

NameTypeRequiredDescription
pageintegerNoPage number (default 1, minimum 1)
limitintegerNoResults per page (default 25, max 100)
searchstringNoSearch term for username/email filtering

Response (JSON)

Paginated user list

FieldTypeDescription
successbooleanTrue on success
usersarrayArray of user account objects
users[].idintegerUser ID
users[].usernamestringUsername
users[].emailstring|nullEmail address
users[].real_namestringReal name
users[].fidonet_addressstring|nullUser's FidoNet address
users[].created_atstringAccount creation timestamp (ISO 8601)
users[].last_loginstring|nullLast login timestamp (ISO 8601)
users[].last_remindedstring|nullLast reminder timestamp (ISO 8601)
users[].is_activebooleanWhether account is active
users[].is_adminbooleanWhether user has admin privileges
users[].is_systembooleanWhether this is a system account
users[].days_since_reminderinteger|nullDays since last reminder was sent
paginationobjectPagination metadata
pagination.pageintegerCurrent page number
pagination.limitintegerResults per page
pagination.totalintegerTotal matching users
pagination.pagesintegerTotal number of pages

Error Responses

StatusDescription
403User is not an admin
401Authentication required
500Database error

GET /api/admin/users/{id}

Requires authentication

Fetches a specific active user's editable fields including username, real name, email, credit balance, status flags, and timestamps. Admin-only. Returns 404 if user not found.

Path Parameters

NameTypeDescription
idintegerUser ID

Response (JSON)

User details for editing

FieldTypeDescription
successbooleanTrue on success
userobjectUser account details
user.idintegerUser ID
user.usernamestringUsername
user.real_namestringReal name
user.emailstring|nullEmail address
user.credit_balanceintegerCurrent credit balance
user.is_activebooleanWhether account is active
user.is_adminbooleanWhether user has admin privileges
user.is_systembooleanWhether this is a system account
user.echomail_moderation_forcedbooleanWhether echomail moderation is forced for this user
user.can_post_netecho_unmoderatedbooleanWhether the user bypasses netecho moderation and posts immediately
user.created_atstringAccount creation timestamp (ISO 8601)
user.last_loginstring|nullLast login timestamp (ISO 8601)

Error Responses

StatusDescription
403User is not an admin
404User not found
401Authentication required
500Database error

POST /api/admin/users/{id}/credits

Requires authentication

Allows admins to manually grant credits to a user with a required note for audit purposes. The credits system must be enabled. Amount must be positive and a descriptive note is mandatory. Credits are recorded with admin adjustment type and include the granting admin's ID.

Path Parameters

NameTypeDescription
idintegerTarget user ID

Request Body (JSON)

Credit grant details

FieldTypeRequiredDescription
amountintegerYesPositive credit amount to grant
notestringYesAudit note explaining the credit grant

Response (JSON)

Credit grant result with success status

FieldTypeDescription
successbooleanWhether credits were granted
new_balanceintegerUser's updated credit balance

Error Responses

StatusDescription
403Requester is not an admin
404Target user not found
400Credits disabled, invalid amount, or missing note

POST /api/admin/users/{id}

Requires authentication

Allows admins to modify user properties including name, email, status flags, and password. Real name is required. Password is optional; if provided, it replaces the current password. Moderation enforcement and system/admin flags can be toggled.

Path Parameters

NameTypeDescription
idintegerUser ID to update

Request Body (JSON)

User update fields

FieldTypeRequiredDescription
real_namestringYesUser's real name
emailstringNoUser's email address
is_activeintegerNo1 for active, 0 for inactive (default: 1)
is_adminintegerNo1 to grant admin, 0 to revoke (default: 0)
is_systemintegerNo1 for system account, 0 otherwise (default: 0)
echomail_moderation_forcedintegerNo1 to force moderation, 0 to allow (default: 0)
can_post_netecho_unmoderatedintegerNo1 to bypass netecho moderation, 0 to require normal moderation rules (default: 0)
passwordstringNoNew password (if provided, updates user's password)

Response (JSON)

Update confirmation

FieldTypeDescription
successbooleanWhether update succeeded

Error Responses

StatusDescription
403Requester is not an admin
404User not found
400Missing required real_name field

POST /api/admin/users/{id}/toggle-status

Requires authentication

Quickly enable or disable a user account. Accepts is_active flag (1 for active, 0 for inactive). Returns success message with action description.

Path Parameters

NameTypeDescription
idintegerUser ID to toggle

Request Body (JSON)

Status toggle

FieldTypeRequiredDescription
is_activeintegerNo1 to enable, 0 to disable (default: 1)

Response (JSON)

Toggle result with localized message

FieldTypeDescription
successbooleanWhether toggle succeeded
message_codestringLocalization key for success message
message_paramsobjectParameters for localized message
message_params.actionstringAction taken: 'enable' or 'disable'

Error Responses

StatusDescription
403Requester is not an admin
404User not found or no rows affected

POST /api/admin/users/create

Requires authentication

Admin endpoint to create user accounts with validation of username format, restricted names, and password strength. Username is normalized per config. Password must be at least 8 characters. Supports setting admin and system flags at creation.

Request Body (JSON)

New user details

FieldTypeRequiredDescription
usernamestringYesUnique username (normalized, validated against format and restrictions)
real_namestringYesUser's real name (validated against restrictions)
passwordstringYesPassword (minimum 8 characters)
emailstringNoUser's email address
is_activeintegerNo1 for active, 0 for inactive (default: 1)
is_adminintegerNo1 to create as admin, 0 otherwise (default: 0)
is_systemintegerNo1 for system account, 0 otherwise (default: 0)

Response (JSON)

New user creation result

FieldTypeDescription
successbooleanWhether user was created
user_idintegerID of newly created user

Error Responses

StatusDescription
403Requester is not an admin
400Missing required fields, invalid username format, restricted name, or password too short
409Username already exists

POST /api/admin/users/cleanup

Requires authentication

Performs cleanup of old rejected registration records while retaining approved registration history. Returns counts of removed records. Useful for maintenance and database hygiene.

Response (JSON)

Cleanup operation results

FieldTypeDescription
successbooleanWhether cleanup completed
resultobjectCleanup statistics
result.approved_removedintegerNumber of approved registration records removed (currently always 0; approved history is retained)
result.old_rejected_removedintegerNumber of old rejected registration records removed
result.total_cleanedintegerTotal records removed
message_codestringLocalization key for success message
message_paramsobjectParameters for localized message
message_params.approvedintegerCount of approved records removed
message_params.rejectedintegerCount of rejected records removed
message_params.totalintegerTotal records removed

Error Responses

StatusDescription
403Requester is not an admin
500Cleanup operation failed

POST /api/admin/users/{userId}/send-reminder

Requires authentication

Sends an account reminder message to a specific user. Checks if user is eligible for reminders before sending. Can send email notification if configured. Returns success status and email delivery confirmation.

Path Parameters

NameTypeDescription
userIdintegerTarget user ID

Response (JSON)

Reminder send result

FieldTypeDescription
successbooleanWhether reminder was sent
message_codestringLocalization key for result message
email_sentbooleanWhether email notification was delivered

Error Responses

StatusDescription
403Requester is not an admin
404Target user not found
400User is not eligible for reminder
500Reminder send failed

GET /api/admin/users/need-reminders

Requires authentication

Retrieves list of users who need account reminders sent. Useful for admin dashboard to identify inactive or at-risk accounts.

Response (JSON)

List of users needing reminders

FieldTypeDescription
successbooleanWhether query succeeded
usersarrayArray of user objects needing reminders
users[].idintegerUser ID
users[].usernamestringUsername
users[].real_namestringReal name
users[].emailstring|nullEmail address
users[].created_atstringAccount creation timestamp (ISO 8601)

Error Responses

StatusDescription
403Requester is not an admin
500Query failed

Verify

MethodPathAuthSummary
GET/api/verifyNoPublic endpoint returning system name and software version for network registry verification.

GET /api/verify

Public

Returns identifying information about the BBS system without requiring authentication. Used by network registries like LovlyNet to verify site ownership and confirm the software in use. Response includes the configured system name and full software version string.

Response (JSON)

System identification data

FieldTypeDescription
system_namestringConfigured BBS system name
softwarestringFull software version string

Whosonline

MethodPathAuthSummary
GET/api/whosonlineYesGet list of users currently online (last 15 minutes).

GET /api/whosonline

Requires authentication

Returns active sessions from the past 15 minutes with user details. Admins receive additional fields including activity description, service type, and last activity timestamp. Non-admin users see only basic user info (id, username, location).

Response (JSON)

Online users and session count

FieldTypeDescription
usersarrayArray of online user objects
users[].user_idintegerUser ID
users[].usernamestringUsername
users[].locationstringUser's location (may be empty)
users[].activitystringCurrent activity description (admin only)
users[].servicestringService type (e.g., 'web', 'binkp') (admin only)
users[].last_activity_tsintegerUnix timestamp of last activity (admin only)
online_user_countintegerTotal count of online users
online_minutesintegerTime window for online status (always 15)

Error Responses

StatusDescription
401Authentication required

GET /api/config/term-menu-keys

Requires authentication

Returns the effective terminal main menu key map for the current system. Used by the Telnet/SSH terminal server at session start to determine which key dispatches which action. Returns the sysop-configured custom map if one is saved; otherwise returns the built-in defaults. Actions absent from the map are disabled.

Response (JSON)

FieldTypeDescription
successbooleanAlways true
term_menu_keysobjectMap of action ID to single-character key string
term_menu_keys.netmailstringKey for Netmail
term_menu_keys.echomailstringKey for Echomail
term_menu_keys.shoutboxstringKey for Shoutbox
term_menu_keys.bulletinsstringKey for Bulletins
term_menu_keys.pollsstringKey for Polls
term_menu_keys.doorsstringKey for Doors
term_menu_keys.filesstringKey for Files
term_menu_keys.settingsstringKey for Settings
term_menu_keys.interestsstringKey for Interests
term_menu_keys.whosonlinestringKey for Who's Online
term_menu_keys.qwkstringKey for QWK offline mail
term_menu_keys.bbsliststringKey for BBS List
term_menu_keys.nodeliststringKey for Nodelist
term_menu_keys.localchatstringKey for Local Chat
term_menu_keys.quitstringKey to quit (always present)

Error Responses

StatusDescription
401Authentication required

POST /api/admin/appearance/term-menu-keys

Requires admin

Saves a custom terminal main menu key map. Each action maps to a unique single ASCII letter (a-z, stored lowercase). Actions omitted from the payload are disabled (hidden from the menu). quit must always be present.

Request Body (JSON)

FieldTypeRequiredDescription
term_menu_keysobjectYesMap of action ID to key char. Known action IDs: netmail, echomail, shoutbox, bulletins, polls, doors, files, settings, interests, whosonline, qwk, bbslist, nodelist, localchat, quit

Response (JSON)

FieldTypeDescription
successbooleantrue on success

Error Responses

StatusDescription
400Invalid key (not a single letter), duplicate key, or quit missing
401Authentication required
403Admin privileges required
500Failed to save settings