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 UNAUTHORIZED Sem header, header malformado, key inexistente, truncada ou revogada.
  • 403 FORBIDDEN Key válida, mas sem o escopo daquele endpoint.

Permissões

A credencial carrega escopos explícitos, e cada endpoint declara o seu.

  • links:read Ler links GET /profile · GET /links · GET /links/{id}
  • links:write Criar, editar e excluir links POST /links · PATCH /links/{id} · DELETE /links/{id} · POST /links/bulk-update
  • categories:read Ler categorias GET /profile · GET /categories · GET /categories/{id}
  • categories:write Criar, editar e excluir categorias POST /categories · PATCH /categories/{id} · DELETE /categories/{id}
  • sections:read Ler as seções da página GET /profile · GET /sections · GET /sections/{id}
  • sections:write Criar, editar, reordenar e excluir seções POST /sections · PATCH /sections/{id} · DELETE /sections/{id}
  • layout:read Ler 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:write Salvar 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}/install
  • analytics:read Ler 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.

Cabeçalhos presentes em toda resposta.
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
  1. A primeira requisição executa e a resposta é guardada.
  2. Repetir a mesma chave com o mesmo corpo devolve a resposta original, com Idempotent-Replayed: true. Nada é criado de novo.
  3. Repetir a mesma chave com corpo diferente responde 409.
  4. Duas requisições simultâneas com a mesma chave: uma executa, a outra recebe 409. Nunca saem dois recursos.
  5. 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.

details aparece só em erro de validação.
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Requisição inválida.",
    "details": [
      {
        "field": "title",
        "message": "Informe o título"
      }
    ]
  }
}
  • VALIDATION_ERROR 400
    Corpo inválido, campo desconhecido, JSON malformado, content-type errado ou query inválida
  • UNAUTHORIZED 401
    Key ausente, malformada, inexistente ou revogada
  • FORBIDDEN 403
    Key válida, mas sem o escopo do endpoint
  • NOT_FOUND 404
    Recurso inexistente ou de outra página
  • CONFLICT 409
    Identidade externa repetida, Idempotency-Key reaproveitada com outro corpo, seção com conteúdo, ou contagem que mudou desde o dryRun
  • RATE_LIMITED 429
    Limite por key ou por IP estourado
  • INTERNAL_ERROR 500
    Erro inesperado, com requestId para investigar

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 /profile 200

    Página e escopos da credencial

    Exige links:read oucategories:read ousections:read oulayout:read ouanalytics: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

Requisição
curl https://arvorede.link/api/v1/profile \
  -H "Authorization: Bearer $ARVOREDE_API_KEY"
Resposta 200
{
  "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 /sections 200

    Listar seções

    Exige sections:read

  • POST /sections 201

    Criar seção

    Exige sections:write

  • GET /sections/{id} 200

    Obter seção

    Exige sections:read

  • PATCH /sections/{id} 200

    Atualizar seção

    Exige sections:write

  • DELETE /sections/{id} 204

    Excluir 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

Requisiçã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"
}'
Resposta 201
{
  "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

Requisição
curl https://arvorede.link/api/v1/sections \
  -H "Authorization: Bearer $ARVOREDE_API_KEY"
Resposta 200
{
  "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

Requisição
curl https://arvorede.link/api/v1/sections/01995e3b-9d22-7b47-a3f1-6c0e8b2d5417 \
  -X DELETE \
  -H "Authorization: Bearer $ARVOREDE_API_KEY"
Resposta 409
{
  "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 /categories 200

    Listar categorias

    Exige categories:read

  • POST /categories 201

    Criar categoria

    Exige categories:write

  • GET /categories/{id} 200

    Obter categoria

    Exige categories:read

  • PATCH /categories/{id} 200

    Atualizar categoria

    Exige categories:write

  • DELETE /categories/{id} 204

    Excluir 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

Requisiçã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"
}'
Resposta 201
{
  "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

Requisiçã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
}'
Resposta 200
{
  "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"
  }
}

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 /analytics 200

    Nú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

Requisição
curl https://arvorede.link/api/v1/analytics \
  -H "Authorization: Bearer $ARVOREDE_API_KEY"
Resposta 200
{
  "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 /layout 200

    Situação do layout

    Exige layout:read

  • DELETE /layout 204

    Voltar ao layout padrão

    Exige layout:write

  • GET /layout/draft 200

    Obter o rascunho

    Exige layout:read

  • PUT /layout/draft 200

    Salvar o rascunho

    Exige layout:write

  • DELETE /layout/draft 204

    Descartar o rascunho

    Exige layout:write

  • POST /layout/draft/publish 200

    Publicar o rascunho

    Exige layout:write

  • POST /layout/draft/preview 201

    Gerar um link de preview

    Exige layout:write

  • POST /layout/validate 200

    Validar um manifesto sem salvar

    Exige layout:read oulayout:write

  • GET /layout/guide 200

    Guia do formato, limites e exemplos

    Exige layout:read oulayout:write

  • GET /layout/versions 200

    Histórico de versões

    Exige layout:read

  • GET /layout/versions/{id} 200

    Obter uma versão

    Exige layout:read

  • POST /layout/versions/{id}/restore 200

    Restaurar 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

Requisição
curl https://arvorede.link/api/v1/layout \
  -H "Authorization: Bearer $ARVOREDE_API_KEY"
Resposta 200
{
  "data": {
    "live": "default",
    "published": null,
    "draft": {
      "revision": 3,
      "updatedAt": "2026-09-20T13:00:00.000Z"
    }
  }
}

Publicar o rascunho

Requisição
curl https://arvorede.link/api/v1/layout/draft/publish \
  -X POST \
  -H "Authorization: Bearer $ARVOREDE_API_KEY" \
  -H "Idempotency-Key: my-agent-publish-3"
Resposta 200
{
  "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 /marketplace 200

    Listar layouts publicados

    Exige layout:read oulayout:write

  • GET /marketplace/{slug} 200

    Obter um layout com o manifesto

    Exige layout:read oulayout:write

  • POST /marketplace/{slug}/install 201

    Instalar 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

Requisição
curl -G https://arvorede.link/api/v1/marketplace \
  -H "Authorization: Bearer $ARVOREDE_API_KEY" \
  --data-urlencode "tag=minimal"
Resposta 200
{
  "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

Requisição
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
}'
Resposta 201
{
  "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.

A key vem de uma variável de ambiente, nunca inline.
export ARVOREDE_API_KEY="arvl_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Montar uma página do zero Seção, categoria dentro dela e o primeiro link. Cada passo devolve o id que o próximo usa.
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"
}'
Descobrir o que já existe A leitura que todo agente faz antes de escrever qualquer coisa.
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"
Mudar muitos links sem surpresa Filtra, simula, confere a contagem e só então aplica. Se alguém mexer na página no meio, o 409 avisa e nada muda.
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
}'
Mexer no layout sem publicar O guia primeiro, o rascunho depois, e um link temporário para conferir no navegador.
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"