Integrate Vetrinà's Image API in 5 Steps

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.

Configure your request

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"
}'
1

Get an API Key

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_KEY
2

Prepare the Product Image

You 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.

3

Pick a Predefined Template & Format

Three core templates are available — same request shape, only templateKey differs:

super-prezzo
Super Prezzo
Prezzo gigante, massimo impatto
sconto-barrato
Sconto
Prezzo barrato + percentuale
nuovo-arrivo
Nuovo Arrivo
Badge nuovo arrivo, look pulito
manifesto
Manifesto
Immagine a tutta pagina e una frase. Senza prezzo: per raccontare il marchio

Formats (all 1080px wide):

4

Call POST /api/v1/generate

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.

5

Handle the Response

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).

6

Integrate Into Your Pipeline

Error codes:

StatusCodeMeaning
401unauthorizedMissing or invalid API key
402upgrade_requiredFree plan quota exhausted (10 images/month). Upgrade to Creator/Agency.
422validation_errorInvalid input — details in response body
429rate_limitedToo many requests — wait Retry-After seconds
500generation_failedRender 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

Claude Implementation Prompt — copy & paste into Claude to implement this integration

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.

Integrations: product → social pipeline

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€ …"
    }
  ]
}
IdempotencyRepeat the same idempotencyKey → no duplicate generation (returns 200 with the existing batch).
PlanCreator or Agency. Each format consumes 1 generation.
ComplianceCaptions 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.

API key scopes

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:readread and download generations
assets:write/upload, background removal, upscale
assets:readread assets and asset jobs
marketplace:readbrowse templates and field schemas
integrations:writepush products from your store
webhooks:readread 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.

Webhooks

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);
    });
}
DeliveryAt-least-once. Deduplicate on x-vetrina-event-id.
RetriesAnything other than 2xx is retried with growing delays (30s → 2h, 6 attempts). After 20 consecutive failures the endpoint is disabled.
RotationRotating a secret signs with both old and new for 24h, so you can roll over without losing events.
DebuggingEvery attempt is logged with payload and response; you can replay any delivery from the UI.

Image services: background removal & upscale

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 } }
Inputmultipart file, or JSON { assetId } / { imageUrl }. Scope assets:write.
Upscalescale 2 or 4. Interpolation, not generation: letterforms and faces are never redrawn. Input side ≤ 4000px, output side ≤ 8000px.
Included vs paidOn 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.

Shopify app

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.

Roadmap: native social publishing & Connected App

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.