{"openapi":"3.1.0","info":{"title":"CapiLove API","version":"1.0.0","description":"API usada pelo próprio aplicativo web do CapiLove para criar, personalizar, pagar e publicar um presente digital (retrospectiva estilo 'wrapped' para casais, amigos ou família). Não é uma plataforma de desenvolvedor separada: é a superfície real que o wizard em /criar chama, documentada para que agentes e integrações entendam o fluxo sem precisar inferir a partir do HTML. Autenticação: a maioria das rotas de escrita usa um `editToken` (UUID) como credencial de posse — não há OAuth nem chave de API. Endpoints internos (painel administrativo, cron, webhook da Stripe) não estão documentados aqui de propósito.","contact":{"name":"CapiLove","url":"https://www.capivaralove.com"}},"servers":[{"url":"https://www.capivaralove.com","description":"Produção"}],"tags":[{"name":"wrapped","description":"Criação e edição do presente (o 'wrapped')."},{"name":"checkout","description":"Pagamento único via Stripe."},{"name":"account","description":"Acesso sem senha a presentes já criados."},{"name":"leads","description":"Captura de contato no funil de criação."}],"paths":{"/api/wrapped/create":{"post":{"operationId":"createWrapped","tags":["wrapped"],"summary":"Cria um rascunho de presente","description":"Cria uma página de presente em rascunho (não publicada) e devolve o `slug` público e o `editToken` — a credencial usada por todas as outras rotas de wrapped/checkout. Não exige login.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"occasion":{"type":"string","enum":["casal","amizade","familia"],"default":"casal"}}}}}},"responses":{"200":{"description":"Rascunho criado.","content":{"application/json":{"schema":{"type":"object","required":["slug","editToken"],"properties":{"slug":{"type":"string","description":"Slug público (URL será /p/{slug} após publicado)."},"editToken":{"type":"string","format":"uuid"}}}}}},"500":{"description":"Falha ao criar o rascunho.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Banco de dados indisponível.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/wrapped/load":{"post":{"operationId":"loadWrapped","tags":["wrapped"],"summary":"Carrega um presente pelo editToken","description":"Devolve o conteúdo completo do presente (blocos, contato, fotos) para reabrir a edição.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["editToken"],"properties":{"editToken":{"type":"string","format":"uuid","description":"Credencial de edição do presente, devolvida por createWrapped. Quem possui o token pode editar o presente — não há login/senha."}}}}}},"responses":{"200":{"description":"Presente encontrado."},"400":{"description":"editToken ausente ou mal formado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Nenhum presente com esse editToken.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Banco de dados indisponível.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/wrapped/save":{"post":{"operationId":"saveWrappedContent","tags":["wrapped"],"summary":"Salva o conteúdo (blocos) do presente","description":"Valida e grava os blocos do presente (capa, contador, estatísticas, galeria, música, linha do tempo, carta, final). `cover`, `counter` e `finale` são obrigatórios; no máximo 12 blocos, sem tipos duplicados.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["editToken","content"],"properties":{"editToken":{"type":"string","format":"uuid","description":"Credencial de edição do presente, devolvida por createWrapped. Quem possui o token pode editar o presente — não há login/senha."},"content":{"type":"object","description":"Ver lib/wrapped/schema.ts (Zod) para o schema completo de cada tipo de bloco.","required":["blocks"],"properties":{"blocks":{"type":"array","maxItems":12,"items":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["cover","counter","stats","gallery","music","timeline","letter","finale"]}}}}}},"startDate":{"type":"string","format":"date","nullable":true},"occasion":{"type":"string","nullable":true}}}}}},"responses":{"200":{"description":"Conteúdo salvo."},"400":{"description":"editToken inválido ou conteúdo que não passa na validação (ver `issues`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Banco de dados indisponível.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/wrapped/upload":{"post":{"operationId":"uploadWrappedPhoto","tags":["wrapped"],"summary":"Envia uma foto para o presente","description":"Recebe multipart/form-data (`editToken`, `file`), encaminha para a Edge Function que remove EXIF/GPS e reencoda em WebP.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["editToken","file"],"properties":{"editToken":{"type":"string","format":"uuid"},"file":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"Foto processada e armazenada."},"400":{"description":"editToken ou file ausente, ou corpo não é multipart/form-data.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"502":{"description":"Falha ao processar a imagem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/wrapped/contact":{"post":{"operationId":"setWrappedContact","tags":["wrapped"],"summary":"Define nome, e-mail e telefone do criador","description":"O par e-mail + telefone vira a credencial de acesso sem senha em /entrar.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["editToken"],"properties":{"editToken":{"type":"string","format":"uuid","description":"Credencial de edição do presente, devolvida por createWrapped. Quem possui o token pode editar o presente — não há login/senha."},"name":{"type":"string","maxLength":120},"email":{"type":"string","format":"email"},"phone":{"type":"string","description":"DDD + número, apenas dígitos."}}}}}},"responses":{"200":{"description":"Contato salvo."},"400":{"description":"editToken, e-mail ou telefone inválidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Banco de dados indisponível.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/wrapped/slug":{"post":{"operationId":"setWrappedSlug","tags":["wrapped"],"summary":"Troca o link (slug) do presente","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["editToken","slug"],"properties":{"editToken":{"type":"string","format":"uuid","description":"Credencial de edição do presente, devolvida por createWrapped. Quem possui o token pode editar o presente — não há login/senha."},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,38}[a-z0-9]$","description":"3 a 40 letras minúsculas, números e hífens."}}}}}},"responses":{"200":{"description":"Slug atualizado."},"400":{"description":"editToken ou slug em formato inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"409":{"description":"Slug já está em uso.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/wrapped/music-search":{"get":{"operationId":"searchMusic","tags":["wrapped"],"summary":"Busca músicas (proxy da iTunes Search API)","description":"Endpoint público e sem autenticação: normaliza resultados da iTunes Search API (título, artista, capa, prévia de 30s).","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string"},"description":"Termo de busca (título/artista). Vazio devolve lista vazia."}],"responses":{"200":{"description":"Resultados da busca (pode ser uma lista vazia).","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"artist":{"type":"string"},"cover":{"type":"string","format":"uri","nullable":true},"preview":{"type":"string","format":"uri","nullable":true}}}}}}}}},"502":{"description":"A iTunes Search API está indisponível.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/checkout/config":{"get":{"operationId":"getCheckoutConfig","tags":["checkout"],"summary":"Chave publicável da Stripe","description":"Configuração pública do checkout, lida em runtime (não embutida no build).","responses":{"200":{"description":"Chave publicável.","content":{"application/json":{"schema":{"type":"object","properties":{"publishableKey":{"type":"string"}}}}}},"503":{"description":"Chave publicável da Stripe não configurada no ambiente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/checkout/payment-intent":{"post":{"operationId":"createPaymentIntent","tags":["checkout"],"summary":"Cria o pedido e o PaymentIntent da Stripe","description":"Cria app.orders (status pending) e o PaymentIntent correspondente. O valor cobrado vem sempre de lib/wrapped/plans.ts no servidor (planos válidos: vitalicio, 24h) — nunca do client.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["editToken","plan"],"properties":{"editToken":{"type":"string","format":"uuid","description":"Credencial de edição do presente, devolvida por createWrapped. Quem possui o token pode editar o presente — não há login/senha."},"plan":{"type":"string","enum":["vitalicio","24h"]}}}}}},"responses":{"200":{"description":"PaymentIntent criado (ou presente já pago).","content":{"application/json":{"schema":{"type":"object","properties":{"clientSecret":{"type":"string"},"orderId":{"type":"string","format":"uuid"},"alreadyPaid":{"type":"boolean"}}}}}},"400":{"description":"editToken ou plan inválidos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Presente não encontrado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"502":{"description":"Falha ao criar o PaymentIntent na Stripe.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/checkout/status":{"get":{"operationId":"getCheckoutStatus","tags":["checkout"],"summary":"Consulta o status de um pedido","description":"Usado para polling entre a confirmação do cartão e a chegada do webhook da Stripe.","parameters":[{"name":"order","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id do pedido, devolvido por createPaymentIntent."}],"responses":{"200":{"description":"Status atual.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["pending","paid","failed","expired"]},"slug":{"type":"string","nullable":true}}}}}},"400":{"description":"Parâmetro order ausente ou inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Pedido não encontrado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/account/login":{"post":{"operationId":"loginByEmailAndPhone","tags":["account"],"summary":"Acesso sem senha aos presentes de um e-mail + telefone","description":"Não há cadastro nem senha: o par e-mail + telefone informado na criação é a credencial.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","phone"],"properties":{"email":{"type":"string","format":"email"},"phone":{"type":"string","description":"DDD + número, apenas dígitos (10 a 13 dígitos)."}}}}}},"responses":{"200":{"description":"Presentes encontrados para esse e-mail + telefone.","content":{"application/json":{"schema":{"type":"object","properties":{"pages":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"editToken":{"type":"string","format":"uuid"},"occasion":{"type":"string","nullable":true},"status":{"type":"string"},"title":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}}}}}}}},"400":{"description":"E-mail ou telefone em formato inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Nenhum presente encontrado com esse e-mail e telefone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}},"/api/leads":{"post":{"operationId":"captureLead","tags":["leads"],"summary":"Registra um lead no funil de criação","description":"Grava e-mail (e opcionalmente nome/telefone/ocasião/editToken) assim que a pessoa entra em /criar, para remarketing de carrinho abandonado.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"},"stage":{"type":"string","enum":["criar_aberto","montando","checkout","comprado"],"default":"criar_aberto"},"name":{"type":"string","maxLength":120},"phone":{"type":"string"},"occasion":{"type":"string","enum":["casal","amizade","familia"]},"editToken":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Lead registrado."},"400":{"description":"Corpo inválido ou e-mail mal formado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"503":{"description":"Banco de dados indisponível.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}}},"components":{"schemas":{"ApiError":{"type":"object","description":"Formato único de erro de toda a API. `error` é sempre uma string legível.","required":["error","code"],"properties":{"error":{"type":"string","description":"Mensagem de erro legível, em português."},"code":{"type":"string","enum":["invalid_request","unauthorized","not_found","conflict","unavailable","upstream_error","internal_error"],"description":"Categoria estável do erro, para tratamento programático."},"hint":{"type":"string","description":"O que tentar a seguir para resolver o erro (quando aplicável)."}}}}}}