Generate on-brand promotional images from a product photo + price via a single HTTP call. Pick a template, fill in your data, copy the snippet, and ship.
Create one → (Creator/Agency plan required)
Or upload via POST /api/v1/upload
Required for sconto-barrato
curl -X POST https://vetrina.stocka.it/api/v1/generate \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"templateKey": "super-prezzo",
"formatKey": "ig-45",
"product": {
"imageUrl": "https://example.com/product.jpg",
"name": "Wireless Headphones"
},
"pricing": {
"price": 49.9,
"originalPrice": 89.9
},
"mode": "final",
"delivery": "signed_url",
"market": "IT",
"adLanguage": "it"
}'Create an API key at /settings/api. The key starts with vk_ and is shown only once — save it immediately. API keys require a Creator (€9.90/mo) or Agency (€39/mo) plan.
Send the key as the x-api-key header on every request:
x-api-key: YOUR_API_KEYYou need a public HTTPS URL for your product photo. Pass it as product.imageUrl. The URL must be accessible by Vetrinà's servers.
If your image is a local file or you want AI background removal, upload it first:
curl -X POST https://vetrina.stocka.it/api/v1/upload \
-H "x-api-key: YOUR_API_KEY" \
-F "file=@product.jpg" \
-F "removeBg=true"Response: { "id": "uuid", "url": "/asset/uuid.png", "width": 1080, "height": 1080, "bgRemoved": true } — use the url value as product.imageUrl. If background removal fails, bgRemoved is false and warnings explains that the original photo was used. Accepted: PNG, JPG, WEBP up to 10MB.
Three core templates are available — same request shape, only templateKey differs:
super-prezzosconto-barratonuovo-arrivomanifestoFormats (all 1080px wide):
Send a POST request with the JSON body. The snippet above is ready to use — just fill in your API key.
Required fields: templateKey, product.imageUrl, pricing.price.
Optional fields: product.name, pricing.originalPrice, pricing.lowestPrice30d, pricing.unitPrice + pricing.unitPriceUnit, badgeText, validUntil (YYYY-MM-DD), layoutChoices, colorOverrides, mode, market, adLanguage.
Kit Social: Pass formats: ["ig-45", "ig-story", "square"] instead of formatKey to generate multiple formats in one request. Each format consumes 1 quota in mode="final" and 0 quota in mode="preview". Response shape changes to { kit: true, images: [...], caption, durationMs }.
Preview mode: Pass mode: "preview" to get a low quality inline base64 PNG with Vetrinà watermark. It does not create a generation, consume quota, or debit wallet; call again with mode: "final" after approval.
Single format (HTTP 200):
{
"id": "a1b2c3d4-...",
"url": "https://...signed-url...",
"asset": {
"signedUrl": "https://...signed-url...",
"expiresAt": "2026-06-28T12:00:00Z",
"contentType": "image/png",
"width": 1080,
"height": 1350
},
"caption": "🔥 Wireless Headphones — 49,90€ instead of 89,90€...",
"width": 1080,
"height": 1350,
"durationMs": 2100
}Preview mode (HTTP 200):
{
"mode": "preview",
"preview": true,
"lowQuality": true,
"watermarked": true,
"url": "data:image/png;base64,...",
"base64": "...",
"caption": "...",
"width": 576,
"height": 720,
"durationMs": 1800
}Delivery modes:
signed_url (default): asset.signedUrl is a temporary URL (24h). Download the PNG and store it in your system.inline_base64: Response includes base64 field with the PNG encoded as base64 — no second download needed.both: You get both asset.signedUrl and base64.The caption field is a ready-to-paste social media caption generated from the same data as the image (price, discount, legal text, hashtags).
Error codes:
| Status | Code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing or invalid API key |
| 402 | upgrade_required | Free plan quota exhausted (10 images/month). Upgrade to Creator/Agency. |
| 422 | validation_error | Invalid input — details in response body |
| 429 | rate_limited | Too many requests — wait Retry-After seconds |
| 500 | generation_failed | Render or upload failed — error message in body |
Rate limits (per organization): 30 generate/min, 3 batch/min, 60 upload/min. On 429, respect the Retry-After header.
Quota: Free = 10 images per month. Creator (€9.90/mo) and Agency (€39/mo) = unlimited. Each format in a Kit Social request consumes 1 generation.
Batch (catalogs): POST /api/v1/batch with { items: [...], webhookUrl?, webhookSecret? } → returns 202 with { batchId, total, statusUrl }. Poll GET /api/v1/batch/{id} or receive a webhook POST signed with x-vetrina-signature: sha256=<HMAC> (HMAC-SHA256 of body with webhookSecret). Max 50 items per batch. Requires Creator plan.
1:1 reproducibility: Every generation is stored with its full contract. The same image can be regenerated identically at any time. Full API reference: /developers · openapi.json
This prompt is pre-filled with your current configuration. Paste it into Claude (or any AI assistant) and it will generate production-ready integration code for your codebase.
You are implementing an integration with the Vetrinà Image Generation API in our codebase.
## API Overview
- **Endpoint**: POST https://vetrina.stocka.it/api/v1/generate
- **Auth**: Header "x-api-key: YOUR_API_KEY" (create at https://vetrina.stocka.it/settings/api — requires Creator or Agency plan)
- **Content-Type**: application/json
- **Response time**: ~2-5 seconds (synchronous)
## Request Body
```json
{
"templateKey": "super-prezzo",
"formatKey": "ig-45",
"product": {
"imageUrl": "https://example.com/product.jpg",
"name": "Wireless Headphones"
},
"pricing": {
"price": 49.9,
"originalPrice": 89.9
},
"mode": "final",
"delivery": "signed_url",
"market": "IT",
"adLanguage": "it"
}
```
## Field Reference
- **templateKey** (required): One of "super-prezzo", "sconto-barrato", "nuovo-arrivo" (predefined templates) or "tpl_<uuid>" (marketplace template)
- **formatKey** (default: "ig-45"): One of "ig-45" (1080×1350, Instagram 4:5), "ig-story" (1080×1920, Story 9:16), "square" (1080×1080, 1:1), "pinterest" (1080×1620, 2:3)
- **mode** (default: "final"): "preview" returns a low quality inline base64 PNG with Vetrinà watermark and does not create generations, consume quota, or debit wallet. "final" generates the high quality output.
- **product.imageUrl** (required): Public HTTPS URL of the product photo, or an asset path returned by POST /api/v1/upload
- **removeBg** (optional): best-effort AI background removal. If it fails, the original photo is used and the response includes warnings.
- **product.name** (optional, max 60 chars): Product display name
- **pricing.price** (required, positive number): Current price
- **pricing.originalPrice** (optional, must be > price): Strikethrough price — activates discount % and Omnibus legal text
- **pricing.lowestPrice30d** (optional, requires originalPrice): Lowest price in last 30 days (EU Omnibus directive)
- **pricing.unitPrice** + **pricing.unitPriceUnit** (optional, both required together): Unit price for food products (e.g. 9.98, "kg")
- **pricing.currency** (default: "EUR"): ISO 4217 currency code
- **badgeText** (optional, max 24 chars): Custom badge text (e.g. "ULTIMI PEZZI")
- **validUntil** (optional, "YYYY-MM-DD"): Offer expiry date — printed on image and in caption
- **delivery** (default: "signed_url"): "signed_url" (24h temporary URL), "inline_base64" (base64 PNG in response), "both"
- **market** (default: "IT"): ISO 3166-1 alpha-2 market code — determines legal regime, currency, date/number formats
- **adLanguage** (default: "it"): Language for legal text and caption — one of: it, en, de, fr, es, nl, pt, pl
- **formats** (optional, array): Kit Social — generate multiple formats in one request (e.g. ["ig-45","ig-story","square"]). Each format consumes 1 quota in mode="final" and 0 quota in mode="preview". Response shape changes to { kit: true, images: [...], caption, durationMs }
- **layoutChoices** (optional): { badgePos: "tr"|"tl", logoPos: "left"|"right" }
- **colorOverrides** (optional): Override brand colors for this image only (primary, primaryDark, accent, badge, ink, paper, paperSoft as hex)
## Response (single format, HTTP 200)
```json
{
"id": "uuid",
"url": "https://...signed-url...",
"asset": {
"signedUrl": "https://...signed-url...",
"expiresAt": "2026-06-28T12:00:00Z",
"contentType": "image/png",
"width": 1080,
"height": 1350
},
"caption": "🔥 Friggitrice ad aria 5,5 L — 49,90€ invece di 89,90€...",
"width": 1080,
"height": 1350,
"durationMs": 2100
}
```
## Response (Kit Social with formats[], HTTP 200)
```json
{
"kit": true,
"images": [
{ "id": "uuid", "formatKey": "ig-45", "url": "...", "asset": {...}, "width": 1080, "height": 1350, "durationMs": 2100 },
{ "id": "uuid", "formatKey": "ig-story", "url": "...", "asset": {...}, "width": 1080, "height": 1920, "durationMs": 1800 }
],
"caption": "...",
"durationMs": 3900
}
```
## Error Codes
- **401**: Unauthorized — missing or invalid API key
- **402**: Quota exceeded (free plan = 10 images per month) or insufficient wallet balance (marketplace templates). Code: "upgrade_required" or "insufficient_wallet_balance"
- **403**: Forbidden — e.g. Agency plan required for brandKitId, or template agreement required for marketplace templates
- **422**: Validation error — details in response body
- **429**: Rate limited — respect the "Retry-After" header (seconds). Limits: 30 generate/min, 3 batch/min, 60 upload/min per organization
- **500**: Generation failed — error message in response body
## Rate Limits
- 30 generate requests / minute (per organization)
- 3 batch requests / minute
- 60 upload requests / minute
- On 429: wait "Retry-After" seconds, then retry
## Quota
- Free plan: 10 images per month (then 402 upgrade_required)
- Creator (€9.90/mo) or Agency (€39/mo): unlimited
- Each format in a Kit Social request consumes 1 generation
## Batch (for catalogs)
- POST /api/v1/batch with { items: [...], webhookUrl?, webhookSecret? }
- Returns 202 with { batchId, total, statusUrl }
- Poll GET /api/v1/batch/{id} or receive webhook POST with header "x-vetrina-signature: sha256=<HMAC-SHA256 of body with webhookSecret>"
- Max 50 items per batch. Requires Creator plan.
## Upload (for binary images)
- POST /api/v1/upload (multipart/form-data, field: "file")
- Accepts PNG, JPG, WEBP up to 10MB
- Optional: removeBg=true for AI background removal. If removal fails, the API still returns 200, uses the original photo, and includes a non-blocking warning.
- Returns { id, url, width, height, bgRemoved, warning?, warnings? } — use the "url" as product.imageUrl
## Task
Implement a function/module that:
1. Takes product data (name, price, optional original price, image URL or file path) as input
2. Calls POST /api/v1/generate with the request body shown above
3. Handles all error codes with appropriate exceptions/messages
4. Retries on 429 with exponential backoff (respecting Retry-After header)
5. Returns the generated image URL (signedUrl) and caption
6. If the image is a local file, upload it first via POST /api/v1/upload, then use the returned URL
Use the configuration values from the request body above as defaults. Make the code production-ready with proper error handling, logging, and type safety.Instead of calling /api/v1/generate per item, let your e-commerce backend push a product whenever it's published. Vetrinà maps it to your configured template/formats, generates the image + caption, and (optionally) delivers a signed publish bundle webhook your system — or a tool like Make/Zapier/Buffer — can post to Instagram & Facebook. Configure defaults at /settings/integrations.
1. On product publish, POST the product:
curl -X POST https://vetrina.stocka.it/api/v1/integrations/products \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{"externalId":"SKU-123","idempotencyKey":"SKU-123@2026-06-29","name":"Friggitrice ad aria 5,5 L","imageUrl":"https://yourshop.example/products/sku-123.png","price":49.9,"originalPrice":89.9,"lowestPrice30d":79.9,"currency":"EUR","description":"Promo of the day"}'2. Response (202): { productEventId, batchId, total, statusUrl }. Poll GET /api/v1/batch/{id}, or receive the publish-bundle webhook when ready.
3. Publish bundle webhook (if configured) — signed with x-vetrina-signature: sha256=<HMAC>:
{
"batchId": "…",
"status": "completed",
"total": 1,
"completed": 1,
"failed": 0,
"context": {
"productEventId": "…",
"externalId": "SKU-123",
"name": "Friggitrice ad aria 5,5 L",
"source": "integration"
},
"results": [
{
"id": "…",
"status": "completed",
"url": "https://…signed…",
"caption": "Friggitrice ad aria 5,5 L a 49,90€ …"
}
]
}| Idempotency | Repeat the same idempotencyKey → no duplicate generation (returns 200 with the existing batch). |
| Plan | Creator or Agency. Each format consumes 1 generation. |
| Compliance | Captions are generated deterministically with the Omnibus disclosure when a strikethrough price + lowestPrice30d is present. |
Native Instagram/Facebook publishing (Vetrinà posts for you) is on the roadmap as a paid add-on — see below.
Each key declares what it may do. Pick the narrowest set that works: if a key leaks, the blast radius is whatever you granted it. Choose scopes when you create the key at /settings/api.
generate:write | /generate, /batch — consumes quota and wallet |
generations:read | read and download generations |
assets:write | /upload, background removal, upscale |
assets:read | read assets and asset jobs |
marketplace:read | browse templates and field schemas |
integrations:write | push products from your store |
webhooks:read | read endpoints and delivery logs |
A call outside a key's scopes returns 403 insufficient_scope listing what was granted. Keys created before scopes existed keep full access (*) and keep working unchanged — nothing to migrate.
A key has no entitlements of its own: it inherits the plan of the account that issued it. GET /api/keys returns an account block telling you whether that key has unlimited generations and whether background removal is included.
Register endpoints at /settings/api/webhooks and subscribe to the events you care about — or to * to receive future ones automatically. Available today: generation.completed, generation.failed, batch.completed, asset.processed, asset.failed, template.moderation.updated.
Verify the signature — header x-vetrina-signature: t=<unix>,v1=<hex>:
import { createHmac, timingSafeEqual } from 'crypto';
// Verify on the RAW body, before parsing JSON.
export function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const timestamp = Number(parts.t);
// Reject anything older than 5 minutes: a captured request must not replay.
if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
// During a secret rotation the header carries TWO v1= values: accept either.
return header
.split(',')
.filter((p) => p.startsWith('v1='))
.some((p) => {
const got = Buffer.from(p.slice(3), 'utf8');
const want = Buffer.from(expected, 'utf8');
return got.length === want.length && timingSafeEqual(got, want);
});
}| Delivery | At-least-once. Deduplicate on x-vetrina-event-id. |
| Retries | Anything other than 2xx is retried with growing delays (30s → 2h, 6 attempts). After 20 consecutive failures the endpoint is disabled. |
| Rotation | Rotating a secret signs with both old and new for 24h, so you can roll over without losing events. |
| Debugging | Every attempt is logged with payload and response; you can replay any delivery from the UI. |
Standalone utilities, asynchronous: they return 202 with a jobId, because a 4× upscale on CPU takes tens of seconds. Who pays depends on the plan of the org that owns the key — paid plans include background removal (unlimited) and a monthly upscale allowance; past that, and on Free, the job is charged to the prepaid wallet (€0.10 background removal, €0.20 upscale). The billedTo field in the 202 says which one happened. If a job fails for good the amount is credited back automatically, and a plan-covered job does not burn the allowance.
# 1. Queue the job (plan allowance first, then wallet; returns immediately)
curl -X POST https://vetrina.stocka.it/api/v1/upscale \
-H "x-api-key: YOUR_API_KEY" \
-F "file=@logo.png" -F "scale=4"
# → 202 { "jobId": "…", "billedTo": "plan", "priceCents": 0, "statusUrl": "/api/v1/asset-jobs/…" }
# (billedTo "wallet" + priceCents 20 once the monthly allowance is used up)
# 2. Poll until completed
curl https://vetrina.stocka.it/api/v1/asset-jobs/JOB_ID \
-H "x-api-key: YOUR_API_KEY"
# → { "status": "completed", "result": { "url": "…", "width": 4096, "height": 4096 } }| Input | multipart file, or JSON { assetId } / { imageUrl }. Scope assets:write. |
| Upscale | scale 2 or 4. Interpolation, not generation: letterforms and faces are never redrawn. Input side ≤ 4000px, output side ≤ 8000px. |
| Included vs paid | On Creator/Agency, removeBg=true on /api/v1/upload stays free — it is a step of a generation you already pay for. On Free it returns 402 background_removal_requires_plan; use this endpoint instead. |
If your merchants are on Shopify they don't need to write any of this. Installing the Vetrinà app connects the store in one OAuth flow: every published product generates its images, and with push to gallery enabled the result lands straight in the product media. Connect it from /settings/integrations. Price changes produce a new image; unrelated edits do not.
Native Instagram & Facebook publishing — connect your Meta accounts and let Vetrinà post single images and daily product carousels directly (with up-to-date descriptions). Planned as a paid add-on metered at €0.50 per image published per network, billed from your prepaid wallet. Today you can already get the same result via the publish-bundle webhook above and your own tooling.
Connected App (OAuth 2.0) — a future flow for third parties to authorize via Vetrinà's account system instead of API keys, with scoped permissions and per-app dashboards. Not available in the current MVP.