API
A API REST do arvorede.link: seções, categorias, links, números e layout de uma página, com autenticação por API key.
Introdução
Uma API REST para agentes, scripts e automações manterem o conteúdo de uma página sem abrir o painel.
Ela não é uma camada paralela: os endpoints chamam as mesmas funções que as telas do painel usam, então slug, posição dentro do grupo e escopo por página se comportam igual nos dois caminhos.
Cada credencial pertence a uma página. O tenant vem
sempre da key: workspaceId enviado no corpo, na query
ou em header é ignorado, e um corpo com campo desconhecido é recusado com 400.
O conteúdo tem três camadas, e todas são opcionais. Um link pode estar numa categoria, direto numa seção ou solto na raiz.
Página
├── Seção /conde/cozinha
│ ├── Categoria /conde/cozinha?categoria=achadinhos
│ │ └── Link
│ └── Link
├── Categoria /conde?categoria=achadinhos
│ └── Link
└── Link https://arvorede.link/api/v1 A versão fica no caminho e não muda por header. Não existe /api/latest. Dentro da v1 entram campos, filtros e endpoints novos; remover, renomear ou mudar tipo
exigiria uma v2.
Os dois são públicos e não exigem autenticação. O OpenAPI é gerado do mesmo código que serve a API.
Authorization: Bearer arvl_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Sem cookie, sem CSRF e sem login: a API é bearer-only, e o cookie de sessão do painel não vale nada aqui.
Formato da key
Toda key começa com arvl_live_, seguida do segredo. O banco
guarda apenas o SHA-256 do token e um prefixo visível, para você reconhecer a credencial na
lista.
- O segredo aparece uma única vez, na criação. Não existe tela nem endpoint que o recupere.
- Perdeu? Revogue e crie outra. A revogação vale na chamada seguinte.
- A key nunca aparece em log, em relatório de erro nem em mensagem da API.
- Guarde como qualquer outro secret: variável de ambiente ou cofre, nunca no Git.
401 UNAUTHORIZEDSem header, header malformado, key inexistente, truncada ou revogada.403 FORBIDDENKey válida, mas sem o escopo daquele endpoint.
Permissões
A credencial carrega escopos explícitos, e cada endpoint declara o seu.
links:readLer links GET /profile · GET /links · GET /links/{id}links:writeCriar, editar e excluir links POST /links · PATCH /links/{id} · DELETE /links/{id} · POST /links/bulk-updatecategories:readLer categorias GET /profile · GET /categories · GET /categories/{id}categories:writeCriar, editar e excluir categorias POST /categories · PATCH /categories/{id} · DELETE /categories/{id}sections:readLer as seções da página GET /profile · GET /sections · GET /sections/{id}sections:writeCriar, editar, reordenar e excluir seções POST /sections · PATCH /sections/{id} · DELETE /sections/{id}layout:readLer o layout, o rascunho e o histórico GET /profile · GET /layout · GET /layout/draft · POST /layout/validate · GET /layout/guide · GET /layout/versions · GET /layout/versions/{id} · GET /marketplace · GET /marketplace/{slug}layout:writeSalvar rascunho, gerar preview e publicar o layout DELETE /layout · PUT /layout/draft · DELETE /layout/draft · POST /layout/draft/publish · POST /layout/draft/preview · POST /layout/validate · GET /layout/guide · POST /layout/versions/{id}/restore · GET /marketplace · GET /marketplace/{slug} · POST /marketplace/{slug}/installanalytics:readLer visitas, cliques e origens GET /profile · GET /analytics
- Escrever não inclui ler. Uma key só com links:write cria e altera links, mas recebe 403 em GET /links.
- Os escopos são independentes entre si: links:write não alcança o layout nem as seções.
- Nenhum escopo dá acesso a conta, senha, email, domínio, exclusão da página ou às próprias credenciais.
Presets na criação
- Somente leitura Consulta links, categorias, layout e números, sem alterar nada.
links:read · categories:read · sections:read · layout:read · analytics:read - Gerenciar conteúdo Lê e altera links, categorias e seções.
links:read · links:write · categories:read · categories:write · sections:read · sections:write - Aparência Lê o conteúdo e altera o layout da página, sem tocar nos links.
links:read · categories:read · sections:read · layout:read · layout:write - Conectar uma IA (MCP) Tudo que o MCP alcança: links, categorias, layout e números da página.
links:read · links:write · categories:read · categories:write · sections:read · sections:write · layout:read · layout:write · analytics:read
Preset é atalho: o que fica gravado são os escopos, um a um.
60 requisições por minuto por credencial, em janela deslizante. Tentativas de autenticação também são limitadas por IP, para conter força bruta em cima de keys.
RateLimit-Limit: 60
RateLimit-Remaining: 57
RateLimit-Reset: 42 Ao estourar, a resposta é 429 RATE_LIMITED com Retry-After em segundos. Espere o tempo indicado em vez
de tentar de novo na hora.
Idempotência
Todo POST aceita um identificador seu para que um retry não duplique nada.
Idempotency-Key: my-agent-air-fryer-001 - A primeira requisição executa e a resposta é guardada.
- Repetir a mesma chave com o mesmo corpo devolve a resposta original, com Idempotent-Replayed: true. Nada é criado de novo.
- Repetir a mesma chave com corpo diferente responde 409.
- Duas requisições simultâneas com a mesma chave: uma executa, a outra recebe 409. Nunca saem dois recursos.
- Se a primeira tentativa falhou, a chave é liberada e o retry pode usá-la.
A chave vale por página, não por credencial: dois agentes da mesma página usando a mesma string compartilham o resultado. Os registros expiram em 24 horas e o limite é de 255 caracteres.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Requisição inválida.",
"details": [
{
"field": "title",
"message": "Informe o título"
}
]
}
} - Corpo inválido, campo desconhecido, JSON malformado, content-type errado ou query inválida
VALIDATION_ERROR400 - Key ausente, malformada, inexistente ou revogada
UNAUTHORIZED401 - Key válida, mas sem o escopo do endpoint
FORBIDDEN403 - Recurso inexistente ou de outra página
NOT_FOUND404 - Identidade externa repetida, Idempotency-Key reaproveitada com outro corpo, seção com conteúdo, ou contagem que mudou desde o dryRun
CONFLICT409 - Limite por key ou por IP estourado
RATE_LIMITED429 - Erro inesperado, com requestId para investigar
INTERNAL_ERROR500
Toda resposta carrega X-Request-Id. Ao relatar um
problema, mande esse valor: é o que amarra a investigação. A resposta nunca traz stack trace,
SQL, nome de tabela nem secret, e respostas autenticadas saem com Cache-Control: private, no-store.
Perfil
Descobre qual página a credencial controla e quais escopos ela carrega.
- GET
/profile200Página e escopos da credencial
Exige
links:readoucategories:readousections:readoulayout:readouanalytics:read
- É a primeira chamada de toda integração: ela confirma a página antes de qualquer escrita.
- Aceita qualquer escopo de leitura.
Descobrir a página conectada
curl https://arvorede.link/api/v1/profile \
-H "Authorization: Bearer $ARVOREDE_API_KEY" {
"data": {
"workspace": {
"id": "01995e3b-6b10-7f33-9a02-11c9d4e5f678",
"handle": "conde",
"name": "Conde",
"publicUrl": "https://arvorede.link/conde"
},
"apiKey": {
"id": "01995e3b-6c21-7d44-8b13-22dae5f60789",
"name": "Automação do Claude",
"scopes": [
"links:read",
"links:write",
"sections:read"
]
}
}
} Seções
Seção é a camada de cima da página: ela agrupa categorias e links, e ganha um endereço próprio em /{handle}/{slug-da-secao}.
- GET
/sections200Listar seções
Exige
sections:read - POST
/sections201Criar seção
Exige
sections:write - GET
/sections/{id}200Obter seção
Exige
sections:read - PATCH
/sections/{id}200Atualizar seção
Exige
sections:write - DELETE
/sections/{id}204Excluir seção vazia
Exige
sections:write
- Uma página pode não ter seção nenhuma. Sem seções, tudo continua na raiz.
- Não existe seção dentro de seção: a hierarquia é Seção → Categoria → Link, com toda camada opcional.
- O slug é estável: renomear a seção não muda o endereço que alguém compartilhou.
- `active: false` tira a seção da página sem excluir: ela some da navegação e o endereço responde 404. Os links dela continuam redirecionando.
- `imageUrl` manda o servidor baixar uma imagem, recortada em quadrado. Num `PATCH`, `null` remove e a ausência mantém.
- Excluir uma seção que ainda tem categorias ou links responde 409 com a contagem do que está dentro. Mova o conteúdo antes.
Criar uma seção
curl https://arvorede.link/api/v1/sections \
-X POST \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-agent-section-kitchen" \
-d '{
"name": "Cozinha",
"description": "Tudo que eu uso para cozinhar"
}' {
"data": {
"id": "01995e3b-9d22-7b47-a3f1-6c0e8b2d5417",
"name": "Cozinha",
"slug": "cozinha",
"description": "Tudo que eu uso para cozinhar",
"active": true,
"imageUrl": null,
"position": 0,
"createdAt": "2026-09-20T13:00:00.000Z",
"updatedAt": "2026-09-20T13:00:00.000Z"
}
} Listar as seções da página
curl https://arvorede.link/api/v1/sections \
-H "Authorization: Bearer $ARVOREDE_API_KEY" {
"data": [
{
"id": "01995e3b-9d22-7b47-a3f1-6c0e8b2d5417",
"name": "Cozinha",
"slug": "cozinha",
"description": "Tudo que eu uso para cozinhar",
"active": true,
"imageUrl": null,
"position": 0,
"createdAt": "2026-09-20T13:00:00.000Z",
"updatedAt": "2026-09-20T13:00:00.000Z"
}
],
"pagination": {
"limit": 50,
"offset": 0,
"total": 1
}
} Excluir uma seção que ainda tem conteúdo
curl https://arvorede.link/api/v1/sections/01995e3b-9d22-7b47-a3f1-6c0e8b2d5417 \
-X DELETE \
-H "Authorization: Bearer $ARVOREDE_API_KEY" {
"error": {
"code": "CONFLICT",
"message": "A seção ainda tem conteúdo. Mova as categorias e os links antes de excluir.",
"details": [
{
"field": "id",
"message": "2 categorias e 5 links"
}
]
}
} Categorias
Categoria é o agrupamento que vira bloco e filtro na página. Ela pode ficar na raiz ou dentro de uma seção.
- GET
/categories200Listar categorias
Exige
categories:read - POST
/categories201Criar categoria
Exige
categories:write - GET
/categories/{id}200Obter categoria
Exige
categories:read - PATCH
/categories/{id}200Atualizar categoria
Exige
categories:write - DELETE
/categories/{id}204Excluir categoria
Exige
categories:write
- `kind` é o tipo semântico, independente do nome visível: uma categoria chamada "Achadinhos" pode ser `shopee`. Leia `kind`, nunca o nome.
- `sectionId` coloca a categoria dentro de uma seção. `null` deixa a categoria na raiz da página.
- Uma seção de outra página responde 400 no campo `sectionId`, igual a uma seção inexistente.
- O slug é estável: ele é o valor de `?categoria=` na página pública.
- Excluir a categoria não exclui os links dela; eles ficam sem categoria.
Criar uma categoria dentro de uma seção
curl https://arvorede.link/api/v1/categories \
-X POST \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-agent-category-deals" \
-d '{
"name": "Achadinhos",
"description": "Promoções do dia",
"kind": "shopee",
"sectionId": "01995e3b-9d22-7b47-a3f1-6c0e8b2d5417"
}' {
"data": {
"id": "01995e3b-8a19-7c55-8d21-9b4e7f10c3d8",
"name": "Achadinhos",
"slug": "achadinhos",
"description": "Promoções do dia",
"kind": "shopee",
"sectionId": "01995e3b-9d22-7b47-a3f1-6c0e8b2d5417",
"active": true,
"position": 0,
"createdAt": "2026-09-20T13:00:00.000Z"
}
} Tirar a categoria da seção
curl https://arvorede.link/api/v1/categories/01995e3b-8a19-7c55-8d21-9b4e7f10c3d8 \
-X PATCH \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sectionId": null
}' {
"data": {
"id": "01995e3b-8a19-7c55-8d21-9b4e7f10c3d8",
"name": "Achadinhos",
"slug": "achadinhos",
"description": "Promoções do dia",
"kind": "shopee",
"sectionId": null,
"active": true,
"position": 0,
"createdAt": "2026-09-20T13:00:00.000Z"
}
} Links
O link é o que o visitante clica. Ele pode morar na raiz, numa seção ou numa categoria.
- GET
/links200Listar links
Exige
links:read - POST
/links201Criar link
Exige
links:write - GET
/links/{id}200Obter link
Exige
links:read - PATCH
/links/{id}200Atualizar link
Exige
links:write - DELETE
/links/{id}204Excluir link
Exige
links:write - POST
/links/bulk-update200Atualizar links em massa
Exige
links:write
- `categoryId` e `sectionId` nunca andam juntos. Com categoria, a seção do link é a da categoria; enviar os dois é 400.
- Publique sempre o `publicUrl`: é ele que conta o clique e redireciona para o destino.
- O slug é estável: mudar o título não muda a URL pública.
- `externalSource` + `externalId` amarram o link ao seu sistema e são únicos por página — use a listagem filtrada para saber se um item já foi publicado.
- `imageUrl` no corpo manda o servidor baixar aquela imagem e guardá-la como mídia da página; a resposta já devolve o endereço em /media. Num `PATCH`, `null` remove a imagem e a ausência mantém.
- `videoUrl` aceita um vídeo do YouTube, TikTok ou Instagram e volta no formato canônico, sem parâmetros de compartilhamento. Nada é baixado: o card ganha o botão “Ver vídeo”, e o clique principal continua indo para o `publicUrl`. `null` remove o vídeo.
- A listagem filtra por `categoryId`, `sectionId` (a seção onde o link aparece), `root`, `featured`, `active`, `q` (título e descrição, sem acento nem caixa), `hasDescription`, `hasVideo`, `externalSource` e `externalId`, todos combináveis. Sem `cursor`, a ordem é a da página: Destaques, depois a raiz, depois cada seção.
- Para percorrer muitos links enquanto altera, pagine com `cursor=start` e siga `pagination.nextCursor` até `null`: a ordem é a de criação e nada se repete nem some no caminho.
- `POST /links/bulk-update` muda até 100 links por id, ou até 500 por filtro. Por filtro, simule com `dryRun: true`, confira o `matched` e aplique com `dryRun: false` e `expectedCount` igual a ele. Nada é excluído em massa.
Criar um link numa categoria
curl https://arvorede.link/api/v1/links \
-X POST \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-agent-air-fryer-001" \
-d '{
"title": "Air fryer 5 litros",
"url": "https://example.com/product",
"description": "A que eu uso todo dia",
"categoryId": "01995e3b-8a19-7c55-8d21-9b4e7f10c3d8",
"coupon": "PROMO10",
"videoUrl": "https://youtu.be/exemplo0001?si=compartilhado",
"featured": true,
"externalSource": "my-agent",
"externalId": "air-fryer-001"
}' {
"data": {
"id": "01995e3b-7c41-7a2e-9b0f-2f3c5d6e74ac",
"title": "Air fryer 5 litros",
"slug": "air-fryer-5-litros",
"description": "A que eu uso todo dia",
"url": "https://example.com/product",
"publicUrl": "https://arvorede.link/conde/go/air-fryer-5-litros",
"categoryId": "01995e3b-8a19-7c55-8d21-9b4e7f10c3d8",
"sectionId": null,
"effectiveSectionId": "01995e3b-9d22-7b47-a3f1-6c0e8b2d5417",
"icon": "bag",
"badge": "Oferta",
"coupon": "PROMO10",
"videoUrl": "https://www.youtube.com/watch?v=exemplo0001",
"featured": true,
"active": true,
"archived": false,
"position": 0,
"externalSource": "my-agent",
"externalId": "air-fryer-001",
"imageUrl": null,
"createdAt": "2026-09-20T13:00:00.000Z",
"updatedAt": "2026-09-20T13:00:00.000Z"
}
} Criar um link solto dentro de uma seção
curl https://arvorede.link/api/v1/links \
-X POST \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Minha newsletter",
"url": "https://example.com/newsletter",
"sectionId": "01995e3b-9d22-7b47-a3f1-6c0e8b2d5417"
}' {
"data": {
"id": "01995e3b-7f52-7c63-9d24-3f4a5b6c7d8e",
"title": "Minha newsletter",
"slug": "minha-newsletter",
"description": null,
"url": "https://example.com/newsletter",
"publicUrl": "https://arvorede.link/conde/go/minha-newsletter",
"categoryId": null,
"sectionId": "01995e3b-9d22-7b47-a3f1-6c0e8b2d5417",
"effectiveSectionId": "01995e3b-9d22-7b47-a3f1-6c0e8b2d5417",
"icon": "bag",
"badge": null,
"coupon": null,
"videoUrl": null,
"featured": false,
"active": true,
"archived": false,
"position": 0,
"externalSource": null,
"externalId": null,
"imageUrl": null,
"createdAt": "2026-09-20T13:00:00.000Z",
"updatedAt": "2026-09-20T13:00:00.000Z"
}
} Categoria e seção no mesmo link
curl https://arvorede.link/api/v1/links \
-X POST \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Air fryer 5 litros",
"url": "https://example.com/product",
"categoryId": "01995e3b-8a19-7c55-8d21-9b4e7f10c3d8",
"sectionId": "01995e3b-9d22-7b47-a3f1-6c0e8b2d5417"
}' {
"error": {
"code": "VALIDATION_ERROR",
"message": "Requisição inválida.",
"details": [
{
"field": "sectionId",
"message": "Um link fica numa categoria ou direto numa seção, nunca nos dois. A seção de um link com categoria é a da categoria."
}
]
}
} Conferir se um item externo já virou link
curl -G https://arvorede.link/api/v1/links \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
--data-urlencode "externalSource=my-agent" \
--data-urlencode "externalId=air-fryer-001" {
"data": [
{
"id": "01995e3b-7c41-7a2e-9b0f-2f3c5d6e74ac",
"title": "Air fryer 5 litros",
"slug": "air-fryer-5-litros",
"description": "A que eu uso todo dia",
"url": "https://example.com/product",
"publicUrl": "https://arvorede.link/conde/go/air-fryer-5-litros",
"categoryId": "01995e3b-8a19-7c55-8d21-9b4e7f10c3d8",
"sectionId": null,
"effectiveSectionId": "01995e3b-9d22-7b47-a3f1-6c0e8b2d5417",
"icon": "bag",
"badge": "Oferta",
"coupon": "PROMO10",
"videoUrl": "https://www.youtube.com/watch?v=exemplo0001",
"featured": true,
"active": true,
"archived": false,
"position": 0,
"externalSource": "my-agent",
"externalId": "air-fryer-001",
"imageUrl": null,
"createdAt": "2026-09-20T13:00:00.000Z",
"updatedAt": "2026-09-20T13:00:00.000Z"
}
],
"pagination": {
"limit": 50,
"offset": 0,
"total": 1
}
} Links de uma categoria ainda sem descrição
curl -G https://arvorede.link/api/v1/links \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
--data-urlencode "categoryId=01995e3b-8a19-7c55-8d21-9b4e7f10c3d8" \
--data-urlencode "hasDescription=false" \
--data-urlencode "cursor=start" {
"data": [
{
"id": "01995e3b-7c41-7a2e-9b0f-2f3c5d6e74ac",
"title": "Air fryer 5 litros",
"slug": "air-fryer-5-litros",
"description": null,
"url": "https://example.com/product",
"publicUrl": "https://arvorede.link/conde/go/air-fryer-5-litros",
"categoryId": "01995e3b-8a19-7c55-8d21-9b4e7f10c3d8",
"sectionId": null,
"effectiveSectionId": "01995e3b-9d22-7b47-a3f1-6c0e8b2d5417",
"icon": "bag",
"badge": "Oferta",
"coupon": "PROMO10",
"videoUrl": "https://www.youtube.com/watch?v=exemplo0001",
"featured": true,
"active": true,
"archived": false,
"position": 0,
"externalSource": "my-agent",
"externalId": "air-fryer-001",
"imageUrl": null,
"createdAt": "2026-09-20T13:00:00.000Z",
"updatedAt": "2026-09-20T13:00:00.000Z"
}
],
"pagination": {
"limit": 50,
"total": 1,
"nextCursor": null
}
} Atualizar só a descrição de vários links
curl https://arvorede.link/api/v1/links/bulk-update \
-X POST \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-agent-descricoes-lote-1" \
-d '{
"items": [
{
"id": "01995e3b-7c41-7a2e-9b0f-2f3c5d6e74ac",
"patch": {
"description": "A que eu uso todo dia"
}
},
{
"id": "01995e3b-7d02-7b11-8c3e-1a2b3c4d5e6f",
"patch": {
"description": "Cabe na bolsa"
}
}
]
}' {
"data": {
"matched": 2,
"updated": 2,
"unchanged": 0,
"failed": 0,
"results": [
{
"id": "01995e3b-7c41-7a2e-9b0f-2f3c5d6e74ac",
"status": "updated"
},
{
"id": "01995e3b-7d02-7b11-8c3e-1a2b3c4d5e6f",
"status": "updated"
}
]
}
} Simular antes de mudar por filtro
curl https://arvorede.link/api/v1/links/bulk-update \
-X POST \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filter": {
"sectionId": "01995e3b-9d22-7b47-a3f1-6c0e8b2d5417",
"hasVideo": true
},
"patch": {
"badge": "Eu uso"
},
"dryRun": true
}' {
"data": {
"dryRun": true,
"matched": 1,
"maxMatched": 500,
"sample": [
{
"id": "01995e3b-7c41-7a2e-9b0f-2f3c5d6e74ac",
"title": "Air fryer 5 litros"
}
]
}
} Números
Visitas, visitantes únicos, cliques e CTR por janela, com os links mais clicados, as origens das visitas e os cliques em cada rede social.
- GET
/analytics200Números da página
Exige
analytics:read
- Só agregado: nunca IP, nunca user agent, nunca o hash do visitante.
- Visitante único é um hash diário cujo sal some na virada do dia — nem o servidor liga o visitante de ontem ao de hoje.
- Navegar dentro da própria página não conta visita nova.
- Clique numa rede social conta em `socials`, não em `clicks`: `clicks` e o CTR continuam sendo só de links.
Ler os números dos últimos 30 dias
curl https://arvorede.link/api/v1/analytics \
-H "Authorization: Bearer $ARVOREDE_API_KEY" {
"data": {
"summary": {
"today": {
"views": 42,
"visitors": 38,
"clicks": 12,
"ctr": 0.29
},
"week": {
"views": 310,
"visitors": 265,
"clicks": 96,
"ctr": 0.31
},
"month": {
"views": 1280,
"visitors": 990,
"clicks": 410,
"ctr": 0.32
}
},
"topLinks": [
{
"id": "01995e3b-7c41-7a2e-9b0f-2f3c5d6e74ac",
"title": "Air fryer 5 litros",
"slug": "air-fryer-5-litros",
"clicks": 87
}
],
"sources": [
{
"source": "instagram.com",
"views": 640
},
{
"source": "direct",
"views": 310
}
],
"sections": [
{
"id": "01995e3b-9d22-7b47-a3f1-6c0e8b2d5417",
"name": "Cozinha",
"slug": "cozinha",
"removed": false,
"active": true,
"views": 420,
"uniqueVisitors": 310,
"clicks": 180,
"ctr": 0.43
},
{
"id": "01995e3b-9d22-7b47-a3f1-000000000000",
"name": "Scooter",
"slug": "scooter",
"removed": true,
"active": false,
"views": 96,
"uniqueVisitors": 74,
"clicks": 21,
"ctr": 0.22
}
],
"socials": [
{
"platform": "instagram",
"clicks": 52
},
{
"platform": "youtube",
"clicks": 18
}
]
}
} Layout
A página inteira é descrita por um Layout Manifest v1. A API lê o que está no ar, valida um manifesto, escreve o rascunho e publica.
- GET
/layout200Situação do layout
Exige
layout:read - DELETE
/layout204Voltar ao layout padrão
Exige
layout:write - GET
/layout/draft200Obter o rascunho
Exige
layout:read - PUT
/layout/draft200Salvar o rascunho
Exige
layout:write - DELETE
/layout/draft204Descartar o rascunho
Exige
layout:write - POST
/layout/draft/publish200Publicar o rascunho
Exige
layout:write - POST
/layout/draft/preview201Gerar um link de preview
Exige
layout:write - POST
/layout/validate200Validar um manifesto sem salvar
Exige
layout:readoulayout:write - GET
/layout/guide200Guia do formato, limites e exemplos
Exige
layout:readoulayout:write - GET
/layout/versions200Histórico de versões
Exige
layout:read - GET
/layout/versions/{id}200Obter uma versão
Exige
layout:read - POST
/layout/versions/{id}/restore200Restaurar uma versão como rascunho
Exige
layout:write
- Escrever nunca publica: `PUT /layout/draft` só mexe no rascunho, e publicar é um passo explícito.
- O rascunho não aparece para visitante nenhum. Para conferir, gere um link de preview — ele vale 1 hora e é uma credencial: não registre a URL em log.
- `POST /layout/validate` roda o mesmo validador da escrita e responde 200 com os erros por caminho do manifesto.
- `GET /layout/guide` entrega o guia em Markdown, os limites e os exemplos — é por onde um agente começa.
- `links:write` não alcança o layout: a apresentação da página exige `layout:write`, pedido explicitamente.
Saber o que está no ar
curl https://arvorede.link/api/v1/layout \
-H "Authorization: Bearer $ARVOREDE_API_KEY" {
"data": {
"live": "default",
"published": null,
"draft": {
"revision": 3,
"updatedAt": "2026-09-20T13:00:00.000Z"
}
}
} Publicar o rascunho
curl https://arvorede.link/api/v1/layout/draft/publish \
-X POST \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
-H "Idempotency-Key: my-agent-publish-3" {
"data": {
"version": {
"id": "01995e3b-a133-7e58-8c35-4d5e6f708192",
"revision": 4,
"live": true
}
}
} Marketplace
Catálogo público de layouts prontos. O mesmo catálogo para todas as páginas: o que muda por workspace é a instalação, que grava no rascunho.
- GET
/marketplace200Listar layouts publicados
Exige
layout:readoulayout:write - GET
/marketplace/{slug}200Obter um layout com o manifesto
Exige
layout:readoulayout:write - POST
/marketplace/{slug}/install201Instalar no rascunho
Exige
layout:write
- Instalar **não publica**. O manifesto entra no rascunho, e a página pública só muda em `POST /layout/draft/publish`.
- Com um rascunho já existente, instalar responde 409. Mande `replaceDraft: true` só depois de a pessoa concordar em descartar o que estava lá.
- O marketplace distribui apenas Layout Manifest v1, que é dado declarativo. Nunca código, script, HTML, CSS livre ou imagem externa.
- A API não publica layouts no marketplace: criar e publicar um anúncio é ato de sessão, no dashboard. Uma API key não vira autora.
- Uma versão publicada é imutável: instalar o mesmo `versionNumber` de novo devolve exatamente o mesmo manifesto.
Procurar um layout
curl -G https://arvorede.link/api/v1/marketplace \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
--data-urlencode "tag=minimal" {
"data": [
{
"slug": "minimal",
"title": "Minimal",
"description": "Perfil centralizado e uma lista só.",
"tags": [
"minimal",
"light"
],
"author": "arvorede.link",
"official": true,
"featured": false,
"installCount": 128,
"publishedAt": "2026-09-20T13:00:00.000Z"
}
],
"pagination": {
"limit": 24,
"offset": 0,
"total": 1
}
} Instalar no rascunho
curl https://arvorede.link/api/v1/marketplace/minimal/install \
-X POST \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"replaceDraft": false
}' {
"data": {
"listing": {
"slug": "minimal",
"title": "Minimal"
},
"versionNumber": 1,
"draft": {
"id": "01995e3b-a133-7e58-8c35-4d5e6f708192",
"status": "draft",
"revision": 1
}
}
} Receitas com curl
Sequências inteiras, para colar no terminal e ver a coisa funcionando.
export ARVOREDE_API_KEY="arvl_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" curl https://arvorede.link/api/v1/sections \
-X POST \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-agent-section-kitchen" \
-d '{
"name": "Cozinha",
"description": "Tudo que eu uso para cozinhar"
}'
curl https://arvorede.link/api/v1/categories \
-X POST \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-agent-category-deals" \
-d '{
"name": "Achadinhos",
"kind": "shopee",
"sectionId": "01995e3b-9d22-7b47-a3f1-6c0e8b2d5417"
}'
curl https://arvorede.link/api/v1/links \
-X POST \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-agent-air-fryer-001" \
-d '{
"title": "Air fryer 5 litros",
"url": "https://example.com/product",
"categoryId": "01995e3b-8a19-7c55-8d21-9b4e7f10c3d8",
"externalSource": "my-agent",
"externalId": "air-fryer-001"
}' curl https://arvorede.link/api/v1/profile \
-H "Authorization: Bearer $ARVOREDE_API_KEY"
curl https://arvorede.link/api/v1/sections \
-H "Authorization: Bearer $ARVOREDE_API_KEY"
curl https://arvorede.link/api/v1/categories \
-H "Authorization: Bearer $ARVOREDE_API_KEY"
curl -G https://arvorede.link/api/v1/links \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
--data-urlencode "limit=100" curl -G https://arvorede.link/api/v1/links \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
--data-urlencode "categoryId=01995e3b-8a19-7c55-8d21-9b4e7f10c3d8" \
--data-urlencode "hasDescription=false" \
--data-urlencode "cursor=start"
curl https://arvorede.link/api/v1/links/bulk-update \
-X POST \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filter": {
"categoryId": "01995e3b-8a19-7c55-8d21-9b4e7f10c3d8",
"hasDescription": false
},
"patch": {
"description": "Achado testado por mim"
},
"dryRun": true
}'
curl https://arvorede.link/api/v1/links/bulk-update \
-X POST \
-H "Authorization: Bearer $ARVOREDE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-agent-descricoes-achadinhos" \
-d '{
"filter": {
"categoryId": "01995e3b-8a19-7c55-8d21-9b4e7f10c3d8",
"hasDescription": false
},
"patch": {
"description": "Achado testado por mim"
},
"dryRun": false,
"expectedCount": 38
}' curl https://arvorede.link/api/v1/layout/guide \
-H "Authorization: Bearer $ARVOREDE_API_KEY"
curl https://arvorede.link/api/v1/layout \
-H "Authorization: Bearer $ARVOREDE_API_KEY"
curl https://arvorede.link/api/v1/layout/draft/preview \
-X POST \
-H "Authorization: Bearer $ARVOREDE_API_KEY"