POST /api/studio/rewrite takes a product (name + current description) and returns an AI-improved description, streamed as Server-Sent Events.
#Request body
| Field | Type | Required | Description |
|---|---|---|---|
productName | string | yes | Product name (max 500) |
currentDescription | string | no | Current description (max 20,000) |
productCategory | string | no | Category (helps AI tone) |
productAttributes | string | no | Attributes (color, size, etc.) |
sallaStoreId | string | no | Salla store id (per-store quota) |
sallaProductId | string | no | Product id (for analytics) |
#SSE Response
The response is text/event-stream with 4 event types:
| Event | Data shape | Meaning |
|---|---|---|
chunk | { "text": "..." } | Piece of the new description, append |
usage | { "input": N, "output": N, "cached": N, "id": "..." } | Token stats (one at the end) |
done | {} | Stream complete |
error | { "message": "..." } | Stream errored |
#Full example
const response = await fetch('/api/studio/rewrite', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
productName: 'Egyptian cotton shirt',
currentDescription: 'Nice shirt with many colors',
productCategory: "Men's fashion",
sallaStoreId: '1234567890',
sallaProductId: '1828908128',
}),
});
if (!response.ok) {
const err = await response.json();
console.error(err);
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let newDescription = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let idx;
while ((idx = buffer.indexOf('\n\n')) >= 0) {
const record = buffer.slice(0, idx);
buffer = buffer.slice(idx + 2);
let eventName = 'message';
let dataStr = '';
for (const line of record.split('\n')) {
if (line.startsWith('event: ')) eventName = line.slice(7);
else if (line.startsWith('data: ')) dataStr += line.slice(6);
}
if (!dataStr) continue;
const data = JSON.parse(dataStr);
if (eventName === 'chunk') newDescription += data.text;
if (eventName === 'usage') console.log('Tokens:', data);
}
}
console.log('Final:', newDescription);#Quota
#Brand voice
To make rewrites match your brand voice:
- 1
Open /studio/brand-voice.
- 2
Upload 5-10 descriptions you love. AI extracts the voice.
- 3
Save. All future rewrites use this voice.
Brand voice is stored per-store in studiobrandvoice.
#Caching
The API uses prompt caching on Anthropic:
- System prompt + brand voice = cached (5min TTL)
- Result: subsequent calls are 80-90% cheaper and faster.
#Errors
| — | "Code" | "Sample |
|---|---|---|
| 401 | { "error": "unauthenticated" } | |
| 400 | { "error": "invalid_request", "message": "..." } | |
| 429 | { "error": "quotaexceeded", "retryafter_hours": 18 } | |
| 500 | { "error": "anthropic_error" } (rare) |