POST /v1/feedback/upload-url
Request a short-lived presigned PUT URL the browser can upload an attachment directly to our object storage with. Pass the resulting key into POST /v1/feedback under attachments[].
/v1/feedback/upload-urlAuth: publicKey + optional userTokenWhy presign?
Screenshots are 100s of KB to a few MB. Routing them through our API would base64-bloat the JSON path, double our ingress cost, and blow the 8 MB submission body cap. Direct browser-to-storage upload sidesteps all three.
Body
| Name | Type | Description |
|---|---|---|
| contentType* | string | MIME type. Must match the regex /^[a-z]+\/[a-z0-9.+-]+$/i. |
| byteSize* | number | Object size in bytes. ≤ 15 MB. |
| kind* | "screenshot" | "annotated" | "attachment" | Categorical tag we use for storage prefix routing and lifecycle policy. |
Response (200)
{
"key": "orgs/69e2.../screenshots/d1c1f9b2-3a8d-4f8a-bb16-9a7d5d09b7cd.png",
"uploadUrl": "https://<storage-endpoint>/userz-private/orgs/.../...png?X-Amz-Algorithm=...",
"expiresInSeconds": 300
}The uploadUrl accepts a single PUT with the same contentType you requested. The key is what you reference in attachments[].key on the subsequent submission.
Browser upload example
async function uploadAndSubmit(blob: Blob, feedbackText: string) {
// 1. Presign
const { key, uploadUrl } = await fetch('https://api.userz.ai/v1/feedback/upload-url', {
method: 'POST',
headers: {
authorization: 'Bearer pub_...',
'content-type': 'application/json',
},
body: JSON.stringify({
contentType: blob.type,
byteSize: blob.size,
kind: 'screenshot',
}),
}).then((r) => r.json());
// 2. PUT to the presigned URL directly. MUST send the same Content-Type.
await fetch(uploadUrl, {
method: 'PUT',
body: blob,
headers: { 'content-type': blob.type },
});
// 3. Submit, referencing the key.
await fetch('https://api.userz.ai/v1/feedback', {
method: 'POST',
headers: {
authorization: 'Bearer pub_...',
'content-type': 'application/json',
},
body: JSON.stringify({
text: feedbackText,
attachments: [
{ key, kind: 'screenshot', contentType: blob.type, byteSize: blob.size },
],
}),
});
}Lifecycle
Objects PUT but never referenced from a Feedback document are swept by the cleanup worker after 24 hours. Attachments referenced from a Feedback document follow the retention policy of the parent feedback (default 30 days; configurable per Org plan).
Errors
| Name | Type | Description |
|---|---|---|
| 400 | invalid_body | Bad contentType regex or oversize byteSize. |
| 401 | (see /v1/feedback) | Same auth errors as the submission endpoint. |
| 429 | rate_limited | Per-IP per-App presign cap exceeded. |