| Location | Name | Required | Meaning |
|---|---|---|---|
| path | id | Yes | Use the ID from an authorized staff response; source validators reject malformed IDs. |
| header | X-Admin-CSRF | Yes | Session-bound token from the staff page; not a developer credential. |
{
"type": "object",
"properties": {
"text": {
"type": "string",
"minLength": 1,
"maxLength": 5000,
"description": "Internal note, never mailed. Service trims and enforces the 5,000-character limit."
}
},
"required": [
"text"
]
}{error:{code,message}} for CloudError failures, attaches Request-Id, and disables caching. Handle 401 by signing in; show role refusal for 403 forbidden, reauthenticate only for step_up_required, and collect a reason for 400 reason_required. A CSRF failure needs a current page/session token, not blind retries. Operation-specific 404/409/413/422/502/503 errors retain their status. Honor Retry-After on 429./** POST /api/admin/support/tickets/:id/note {text} (support.reply): an internal note, never mailed. Audited support.note. */
export const POST = adminRoute("support.reply", async (request, ctx, { params }: Context) => {
const id = ticketIdParam((await params).id);
const { text } = await readBody(request, z.object({ text: z.string().min(1).max(MAX_NOTE + 100) }));
return adminJson(await addNote(ctx, id, text), { status: 201 });
});