Issues API
Submit a feature request or bug report. No account, token, or development key is required.
Submit an issue
POST /issues accepts a JSON object with
Content-Type: application/json. Every successful submission creates a new issue.
Each client IP may submit one issue per 60 seconds, across all issue types, email addresses,
and visibility settings. Extra submissions return 429 with a
Retry-After header containing the remaining wait in seconds.
Invalid submissions do not consume the allowance, and blocked retries do not extend it.
The limit is stored in the database and shared across API instances and restarts.
curl -X POST "https://tucan.amlang.net/api/issues" \
-H 'Content-Type: application/json' \
--data-binary '{
"email": "user@example.com",
"title": "Add keyboard shortcuts for build targets",
"description": "I would like to switch build targets without opening the menu.",
"type": "feature_request",
"visibility": "public"
}'
For a bug report, use "type": "bug" and describe the steps to reproduce,
expected behavior, actual behavior, and your platform and application version.
Required fields
| field | requirements |
|---|---|
email | Valid contact email address, at most 254 characters. |
title | Nonempty text, at most 200 characters. |
description | Nonempty text, at most 10,000 characters. Newlines are preserved. |
type | Exactly feature_request or bug. |
All four fields must be JSON strings. Leading and trailing whitespace is removed.
Unknown fields are ignored. The server assigns id and createdAt.
Submitting an email does not create an account, verify the address, or send an email.
Optional visibility is public or private;
omitted means private. New issues always have approvalStatus: "pending_approval"
and status: "suggestion".
Sending approvalStatus or status on submission returns 400;
only an administrator can set these fields.
Response
201 Created returns the saved issue as JSON:
{
"id": 42,
"email": "user@example.com",
"title": "Add keyboard shortcuts for build targets",
"description": "I would like to switch build targets without opening the menu.",
"type": "feature_request",
"visibility": "public",
"approvalStatus": "pending_approval",
"status": "suggestion",
"createdAt": "2026-09-21T10:00:00Z"
}
Keep the returned ID as a reference. Retrying a successful request after the rate-limit interval creates another issue.
Public issues
curl "https://tucan.amlang.net/api/issues"
curl "https://tucan.amlang.net/api/issues/42"
GET /issues returns a JSON array of all issues that are both
visibility: "public" and approvalStatus: "approved", newest first (or []).
GET /issues/{id} returns one issue with the same restriction.
No authentication is needed. These responses include the issue fields shown above
except email, which is never included in public reads.
Private, pending, rejected, and nonexistent issues all return 404 on
public reads, even when an admin token is supplied. Query parameters cannot bypass
the visibility and approval checks. Marking an issue private or withdrawing approval
removes it from subsequent public reads; responses use Cache-Control: no-store.
The title and description of an approved public issue are public content. Anonymous submitters cannot edit issues after submission.
Moderation
These endpoints require the existing X-Admin-Token header.
Admin responses include email addresses, ipAddress, and all visibility and approval states.
The server records the submission address in the ip_address database column.
It prefers X-Real-IP, which the edge nginx overwrites with
$remote_addr. This avoids trusting a caller-supplied first address in
X-Forwarded-For, to which nginx appends the client address.
If X-Real-IP is absent, the API falls back to the first address in
X-Forwarded-For, then the connection's remote address. This trust model
assumes requests come through the configured nginx; direct API access must be restricted
before relying on these headers for IP blocking.
Missing or oversized addresses, and issues created before IP recording, have a null address.
IP addresses are excluded from submission and public read responses. Request JSON
cannot set or change the recorded address. The same address is used for the submission rate limit;
submissions with no usable recorded address share a single fallback allowance.
curl "https://tucan.amlang.net/api/admin/issues" -H "X-Admin-Token: $ADMIN_TOKEN"
curl "https://tucan.amlang.net/api/admin/issues/42" -H "X-Admin-Token: $ADMIN_TOKEN"
curl -X PATCH "https://tucan.amlang.net/api/admin/issues/42" \
-H "X-Admin-Token: $ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
--data-binary '{"approvalStatus":"approved"}'
PATCH /admin/issues/{id} accepts any combination of visibility,
approvalStatus, and status (at least one is required).
Visibility is public or private; approval status is
pending_approval, approved, or rejected.
Omitted fields are preserved. Approval alone does not make a private issue public.
Successful moderation returns 200 with the updated issue.
Other fields, including the email and issue text, are not changed by this endpoint.
Workflow status
status tracks the work independently of approvalStatus.
It is returned in submission, public, and admin responses. New issues start as
suggestion. Administrators can move an issue to any of these stages:
| status value | stage |
|---|---|
suggestion | Suggestion |
backlog | Backlog |
in_progress | In progress |
in_review | In review |
in_testing | In testing |
done | Done |
curl -X PATCH "https://tucan.amlang.net/api/admin/issues/42" \
-H "X-Admin-Token: $ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
--data-binary '{"status":"in_progress"}'
Changing workflow status does not change approval or visibility. Even a done
issue is only publicly readable when it is public and approved. Invalid updates
return 400 without changing any field.
Errors
| status | meaning |
|---|---|
400 | Invalid JSON object, missing or invalid field, unsupported type, or text exceeding its limit. No issue is created. |
415 | Send the request with Content-Type: application/json. |
429 | This IP has submitted an issue within the last 60 seconds. Wait for the number of seconds in Retry-After before retrying. |
403 | Missing or invalid admin token on an admin endpoint. |
404 | Issue does not exist, or is not publicly available through the public endpoint. |
Errors return JSON describing the first validation failure, for example:
{"error":"type must be feature_request or bug"}