Signed-in supplier staff and buyers can send a bug or a piece of feedback from any page in the app. Each report is stored with status open and emailed to Happy Webs.
This page is for the automation that picks those reports up. It is not a workspace API key, and it is not in the OpenAPI document.
AuthenticateLink to this section#
The request uses a bearer token. The token is the Worker secret FEEDBACK_REPORTS_TOKEN.
Set it once, out of git:
wrangler secret put FEEDBACK_REPORTS_TOKEN
For local development, put the same name in .dev.vars. Do not commit the value.
Send it on every request:
GET /api/feedback-reports?since=2026-10-15T00:00:00.000Z HTTP/1.1
Host: tradecatalog.app
Authorization: Bearer <FEEDBACK_REPORTS_TOKEN>
curl "https://tradecatalog.app/api/feedback-reports?since=2026-10-15T00:00:00.000Z" \
-H "Authorization: Bearer $FEEDBACK_REPORTS_TOKEN"
If the secret is missing, or the header is missing or wrong, the response is 401 with WWW-Authenticate: Bearer realm="feedback-reports". There is no cookie, no workspace API key, and no CORS.
GET/api/feedback-reports
List open reportsLink to this section#
since is required. It is an ISO-8601 time or a Unix time in milliseconds. The list is open reports created after that time, oldest first, up to 100.
| Name | Type | Required | Description |
|---|---|---|---|
since | string | yes | Exclusive lower bound on createdAt. |
after | string | no | Report id. With since, also returns reports that share that exact time and sort after this id. Use both when hasMore is true. |
A report that has been closed is not in this list, even if it was created after since.
{
"reports": [
{
"id": "…",
"kind": "bug",
"status": "open",
"message": "The price shows as zero.",
"pageUrl": "https://tradecatalog.app/s/northgate/products",
"tenantSlug": "northgate",
"userId": "…",
"role": "Owner",
"email": "owner@example.com",
"userAgent": "…",
"viewport": "1440x900",
"createdAt": "2026-10-15T12:00:00.000Z",
"hasScreenshot": true,
"screenshotUrl": "https://tradecatalog.app/api/feedback-reports/…/screenshot",
"autoReplyText": null,
"autoReplySentAt": null,
"resolvedReplyText": null,
"resolvedReplySentAt": null,
"adminUrl": "https://tradecatalog.app/admin?section=reports&report=…"
}
],
"hasMore": false
}
Store the createdAt and id of the last report you kept. The next poll uses that time as since. If the page was full (hasMore is true), also pass that id as after and keep the same since, so a report saved in the same millisecond is not skipped.
ScreenshotLink to this section#
GET/api/feedback-reports/{id}/screenshot
The same bearer token. The response is the image, or 404 when that report has no screenshot. The id is the report id from the list. It is not an object key.
Platform admins can also open the screenshot while signed in at /admin/feedback-reports/{id}, and resolve a report under Support → Reports.
Automatic replyLink to this section#
Saving a report emails the signed-in person one short note at their own address. Resolving it emails them once more, to say it is fixed and live. Each of those is sent at most once per report.
autoReplyText and autoReplySentAt are that first note. resolvedReplyText and resolvedReplySentAt are the resolved note. They stay null until the email has been sent.
Set the Worker variable FEEDBACK_AUTO_REPLY to off to stop both. AUTO_REPLY_BCC is a secret: the address copied on those emails. Leave it unset and nothing is copied. The wording is drafted with Workers AI. If that draft fails a check, a fixed note is sent instead.
Each report also becomes a numbered ticket. Resolving that ticket sends the same fixed-and-live email once. Mail to support@ or info@tradecatalog.app is forwarded to SUPPORT_FORWARD_TO before it is read, then filed the same way. If that address is missing, or the forward fails, the message is rejected so the sender can retry. It is attached to an existing account only when Authentication-Results shows a DMARC pass, or both DKIM and SPF when DMARC is absent. Otherwise the ticket stays unlinked and no acknowledgement is sent.