Mídia

A Mídia é o utilitário de upload compartilhado da API, para imagens e vídeos. Em vez de cada recurso ter seu próprio upload, você sobe o arquivo uma vez aqui, recebe um identificador e depois vincula esse identificador ao recurso final.

Hoje quem consome a Mídia é:

Imagem e vídeo têm fluxos diferentes: a imagem sobe numa única chamada multipart/form-data; o vídeo sobe direto no storage por URL pré-assinada, em três chamadas. Comece pelas imagens abaixo e veja Vídeos para o segundo fluxo.


Como funciona

O upload é síncrono e em uma única chamada (multipart/form-data): você manda o arquivo e, na mesma resposta, recebe a imagem já processada. O servidor converte tudo para WebP, gera automaticamente as variações de tamanho (thumbnail, sm, md, lg, xl) e devolve as URLs públicas de cada uma. Você não precisa redimensionar nada do seu lado.

O identificador da imagem é o campo id (um ULID). É esse valor que você usa como image_id ao vincular a um produto ou anúncio, e como {file_hash} nas rotas de detalhe e exclusão.


Ciclo de vida

A imagem nasce solta (in_use: false, sem reference_id). Ela só passa a contar como "em uso" quando você a vincula a um recurso. Por exemplo, ao adicioná-la à galeria com POST /api/products/{id}/gallery passando o id como image_id.

  • Solta: imagens sem vínculo são removidas por uma rotina de limpeza da plataforma (janela de ~72h). Suba a imagem só quando for usá-la em seguida.
  • Em uso: uma imagem in_use: true fica protegida: qualquer tentativa de excluir ou alterar retorna 409. É uma trava de segurança: evita que você derrube sem querer uma foto que está aparecendo num produto ou anúncio publicado. Quando quiser mesmo remover, tire o vínculo no recurso primeiro (ex.: remova da galeria do produto) e aí a exclusão libera.

Subir uma imagem

POST/api/media/upload

Envie como multipart/form-data no campo image:

RegraValor
Campoimage (arquivo)
Formatos aceitosimage/jpeg, image/png, image/webp
Tamanho máximo10 MB
curl -X POST https://api.unisupri.com/api/media/upload \
  -H "Authorization: Bearer sk_live_a1b2c3d4e5f6..." \
  -F "image=@/caminho/para/foto.jpg"

Resposta 201. O arquivo já vem convertido para WebP, com uma URL por tamanho:

{
  "success": true,
  "data": {
    "id": "01K8PBIMG00001VWXYZ12345678",
    "original_source": "https://cdn.unisupri.com/products/2026/05/28/original_01K8PBIMG00001VWXYZ12345678.webp",
    "urls": [
      { "name": "thumbnail", "size": "135x135", "url": "https://cdn.unisupri.com/.../thumbnail_01K8PBIMG....webp" },
      { "name": "sm", "size": "200x200", "url": "https://cdn.unisupri.com/.../sm_01K8PBIMG....webp" },
      { "name": "md", "size": "400x400", "url": "https://cdn.unisupri.com/.../md_01K8PBIMG....webp" },
      { "name": "lg", "size": "800x800", "url": "https://cdn.unisupri.com/.../lg_01K8PBIMG....webp" },
      { "name": "xl", "size": "1600x1600", "url": "https://cdn.unisupri.com/.../xl_01K8PBIMG....webp" }
    ],
    "in_use": false,
    "reference_id": null,
    "reference_model": null,
    "created_at": "2026-05-28T10:00:00-03:00",
    "updated_at": "2026-05-28T10:00:00-03:00"
  }
}

Listar imagens

GET/api/media

Lista paginada das imagens da sua loja. Filtros:

ParâmetroPara quê
filter[reference_id]Imagens vinculadas a um recurso específico (ID do produto/anúncio).
filter[reference_model]Tipo do recurso vinculado. Os que interessam à integração: product (produto), publication (anúncio) e store (loja).
filter[in_use]true (já vinculadas) ou false (soltas).
sortcreated_at ou updated_at (prefixe com - para descendente).
per_pageItens por página (1 a 100, default 15).

Reaproveitar fotos de SKUs

Duas rotas atalham o caso "já tenho as fotos nesse SKU, quero as mesmas em outro lugar", sem baixar e reenviar arquivo:

O GET consulta, para uma lista de SKUs da sua loja, as imagens já na galeria de cada um. SKU que não é seu volta com status: "sku_not_found" e lista vazia. O POST cria cópias dessas imagens (novas imagens, novos ids) para você vincular onde quiser. Campos e limites nos schemas da referência.


Detalhes de uma imagem

GET/api/media/{file_hash}

Retorna os mesmos dados do upload para a imagem cujo id você informar. 404 se ela não existir ou não pertencer à sua loja.


Excluir uma imagem

DELETE/api/media/{file_hash}

Remove o registro e os arquivos no storage. Retorna 204. Se a imagem estiver em uso por um produto ou anúncio (in_use: true), retorna 409: desvincule antes.


Vídeos

Um vídeo por produto (SKU) e um por anúncio. O identificador é o video_id (também um ULID), e o ciclo de vida é o mesmo das imagens: nasce solto, vira in_use quando você vincula, e vídeo solto é limpo pela rotina da plataforma.

A diferença está no upload. O arquivo de vídeo é grande demais para passar pela API, então ele vai direto para o storage por uma URL pré-assinada, o mesmo mecanismo dos anexos do Chat.

Limites

Não há conversão do nosso lado: o arquivo é aceito ou recusado como veio. Converta antes de enviar.

RegraValor
Formatovideo/mp4
Codec de vídeoH.264
Codec de áudioAAC, MP3 ou sem áudio
Tamanho máximo100 MB
Duração máxima90 segundos
Validade da URL de upload15 minutos

O fluxo em três chamadas

1. Reserve o slot. Informe nome, tipo e tamanho do arquivo:

curl -X POST https://api.unisupri.com/api/media/videos/intent \
  -H "Authorization: Bearer sk_live_a1b2c3d4e5f6..." \
  -H "Content-Type: application/json" \
  -d '{ "file_name": "demo.mp4", "mime_type": "video/mp4", "size_bytes": 8342100 }'

Resposta 201:

{
  "success": true,
  "data": {
    "video_id": "01K8PBVID00001VWXYZ12345678",
    "upload_url": "https://cdn.unisupri.com/cdn-content/2026/08/17/video_01K8PBVID00001VWXYZ12345678.mp4?X-Amz-Signature=...",
    "method": "PUT",
    "expires_at": "2026-08-17T10:15:00-03:00"
  }
}

2. Suba o arquivo.PUT no upload_url, com o binário no corpo, sem o header Authorization (a assinatura já está na URL):

curl -X PUT "https://cdn.unisupri.com/cdn-content/..." \
  -H "Content-Type: video/mp4" \
  --upload-file /caminho/para/demo.mp4

3. Confirme. Só então a plataforma conta o vídeo como enviado:

curl -X POST https://api.unisupri.com/api/media/videos/01K8PBVID00001VWXYZ12345678/confirm \
  -H "Authorization: Bearer sk_live_a1b2c3d4e5f6..."

O confirm relê tamanho e tipo direto do storage: o que você declarou no passo 1 não é palavra final. Arquivo que não chegou ao storage volta 422; arquivo que estourou os 100 MB volta 422 e é descartado do storage. Passando, o vídeo entra na fila de processamento. A chamada é idempotente: repetir o confirm não reprocessa nada.

Estados do vídeo

O processamento é assíncrono: o confirm devolve o vídeo em uploaded/processing, e é o status que diz quando ele pode ser usado.

statusO que significa
reservedSlot criado no intent, arquivo ainda não enviado.
uploadedArquivo no storage, na fila de processamento.
processingEm análise (codec, duração, extração da capa).
readyPronto para vincular. Só neste estado o vídeo pode ir para um produto ou anúncio.
rejectedRecusado por regra de conteúdo (codec, áudio ou duração). O motivo vem em rejection_reason.
failedErro no processamento. Suba de novo.

Consulte com GET/api/media/videos/{id} até chegar a ready (alguns segundos, na prática). 404 se o vídeo não for da sua loja.

{
  "success": true,
  "data": {
    "id": "01K8PBVID00001VWXYZ12345678",
    "url": "https://cdn.unisupri.com/cdn-content/2026/08/17/video_01K8PBVID00001VWXYZ12345678.mp4",
    "duration_seconds": 32.5,
    "width": 1080,
    "height": 1920,
    "poster": {
      "id": "01K8PBIMG00009VWXYZ12345678",
      "original_source": "https://cdn.unisupri.com/.../original_01K8PBIMG....webp",
      "resources": [
        { "name": "sm", "size": "200x200", "url": "https://cdn.unisupri.com/.../sm_01K8PBIMG....webp" },
        { "name": "lg", "size": "800x800", "url": "https://cdn.unisupri.com/.../lg_01K8PBIMG....webp" }
      ]
    },
    "status": "ready",
    "rejection_reason": null,
    "size_bytes": 8342100
  }
}

O poster é a capa: um frame extraído do vídeo e publicado como imagem WebP nos tamanhos de sempre. Use no atributo poster do seu player. Ele é best-effort: se a extração falhar o vídeo continua ready, só que com poster: null. Trate esse caso no front.

status, rejection_reason e size_bytes só aparecem para você, o dono. Nas respostas públicas (vitrine, página do produto) o vídeo vem sem esses campos, e vídeo que não está ready simplesmente não aparece.

Vincular a um produto ou anúncio

PUT/api/products/{id}/gallery/video

Com o video_id em ready, mande { "video_id": "01K8PB..." } para vincular ao SKU, e { "video_id": null } para desvincular. Para anúncios, o campo video_id vai no corpo do create/update: veja Anúncios › Imagens.

ErroQuando
404O video_id não existe ou não é da sua loja. (No create/update de anúncio, id inexistente volta 422 pela validação do payload; id de outra loja volta 404.)
422O vídeo ainda não está ready (ou foi rejected/failed).
409O vídeo já está vinculado a outro produto ou anúncio.

Um vídeo pertence a um único recurso. Para usar o mesmo conteúdo no produto e no anúncio, veja a clonagem abaixo.

Reaproveitar o vídeo de um SKU

POST/api/media/videos/clone-from-sku

Mesma ideia do clone de fotos, na versão de vídeo: informe { "sku": "..." } e a plataforma copia o vídeo daquele SKU num registro novo (arquivo e capa próprios), pronto para vincular a um anúncio. Sem download nem reenvio.

Sempre 200, com um status:

statusO que aconteceu
clonedCópia criada. O vídeo vem em data.video.
no_videoO SKU não tem vídeo, ou o vídeo ainda não está ready.
sku_not_foundO SKU não existe ou não é da sua loja.
failedA cópia no storage falhou. Tente de novo.

O GET/api/media/skus-gallery também devolve o campo video de cada SKU: dá para descobrir num só request o que existe antes de clonar.

Excluir um vídeo

DELETE/api/media/videos/{id}

Remove o registro, o arquivo e a capa. Retorna 204. Vídeo em uso por um produto ou anúncio retorna 409: desvincule antes (video_id: null no recurso).

Retenção

  • Um intent que nunca recebeu upload (reserved) é descartado depois de ~1 hora. Reserve o slot na hora de subir o arquivo, não antes.
  • Um vídeo solto (nunca vinculado) é removido pela limpeza diária, na mesma janela das imagens (~72h).