API do arvore.link

para automatizar o que faria na mão: criar e editar links curtos, montar árvores e ler seus próprios cliques. de graça, sem depender de plano pago para ter acesso.

índice e busca

comece aqui

  1. 1. crie sua conta — é só um código no e-mail, sem senha.
  2. 2. vá em painel → API e crie um token. ele aparece uma única vez: guardamos só um hash, então nem nós conseguimos mostrá-lo de novo.
  3. 3. mande o token no cabeçalho de cada requisição.
curl -X POST https://api.arvore.link/links \
  -H "Authorization: Bearer arv_seu_token" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://exemplo.com/uma-pagina"}'

# {"id":"a1b2c3d4","slug":"x7k2","short_url":"https://arvr.ink/x7k2", ...}

autenticação

um token começa com arv_ e não expira. ele faz o que você faz com links, árvores e métricas — mas não administra a conta: trocar e-mail, apagar a conta, encerrar sessões e criar outros tokens respondem 403 e exigem login de verdade.

isso não é uma limitação por acaso: é o que faz a revogação valer. um token vazado não consegue criar um segundo token para sobreviver à revogação do primeiro.

limites

no plano gratuito: um token ativo por conta e 100 requisições por hora por token. passando disso, a resposta é 429 com {"error":"RATE_LIMITED"} e um cabeçalho Retry-After em segundos.

revogar um token libera a vaga, então trocar de token é revogar e criar — o registro do antigo continua na lista, com a data em que parou de valer.

parâmetros UTM

não existe um campo de UTM na API. os parâmetros fazem parte do próprio destino, então monte a url completa e mande em url:

curl -X POST https://api.arvore.link/links \
  -H "Authorization: Bearer arv_..." \
  -H "Content-Type: application/json" \
  -d '{"url":"https://exemplo.com.br/promo?utm_source=instagram&utm_medium=social"}'

é de propósito: guardar os parâmetros separados criaria duas versões da mesma informação, que podem discordar. o painel faz exatamente isso — junta tudo na url antes de enviar — e lê de volta a partir dela quando você abre o link para editar.

o campo de referral escreve ref, não utm_referral — esse último não é lido por nenhuma ferramenta de análise.

expiração

expires_at é em milissegundos, não em segundos. uma data no passado é recusada com EXPIRES_AT_IN_THE_PAST — quase sempre é erro de fuso, e aceitar criaria um link já morto.

curl -X PATCH https://api.arvore.link/links/a1b2c3d4 \
  -H "Authorization: Bearer arv_..." \
  -H "Content-Type: application/json" \
  -d '{"expires_at":1785400000000,"expiration_url":"https://exemplo.com.br/acabou"}'

depois da data, o link responde 302 para expiration_url, ou 404 se não houver um. é 302 e não 301 de propósito: um permanente fica no cache do navegador e continuaria valendo mesmo depois de você adiar ou remover a expiração. o clique continua sendo contado nos dois casos — tráfego numa campanha que acabou é justamente o que vale saber.

expiration_url sozinho é recusado com EXPIRATION_URL_WITHOUT_EXPIRY: sem data ele nunca seria usado.

no PATCH, todo campo opcional segue as mesmas três regras: omitir mantém o que está gravado, null apaga, e um valor grava. vale para expires_at, expiration_url e tags (que usa [] para apagar).

segmentação

um link, destinos diferentes por sistema operacional e por país:

curl -X PATCH https://api.arvore.link/links/a1b2c3d4 \
  -H "Authorization: Bearer arv_..." \
  -H "Content-Type: application/json" \
  -d '{"targeting":{
        "ios":"https://apps.apple.com/app/id123",
        "android":"https://play.google.com/store/apps/details?id=x",
        "geo":{"PT":"https://exemplo.pt"}
      }}'

o sistema operacional tem prioridade sobre o país. um iPhone no Brasil que casa com a regra de iOS e com a de BR vai para a App Store — a regra de país é a afirmação mais ampla, então ela perde. quem não casa com nenhuma regra vai para o url normal, que continua obrigatório.

a regra de ios vale também para iPadOS — mandar um iPad para a Play Store é exatamente o erro que essa função existe para evitar. os códigos de país são ISO 3166-1 alfa-2 e são gravados em maiúsculas, porque é assim que o CloudFront informa o país — uma chave minúscula seria uma regra que nunca casa. no máximo 50 países por link.

targeting é substituído por inteiro, não mesclado: mande o conjunto completo de regras, ou null para remover todas. mesclar tornaria impossível apagar um país, já que não haveria como dizer que ele não existe mais.

um link expirado ignora a segmentação: acabou para todo mundo. cada clique registra qual regra decidiu o destino, então /analytics responde se a segmentação está funcionando de verdade.

senha

um link com senha para de redirecionar direto: quem abrir vê um formulário e só depois é levado ao destino.

curl -X PATCH https://api.arvore.link/links/a1b2c3d4 \
  -H "Authorization: Bearer arv_..." \
  -H "Content-Type: application/json" \
  -d '{"password":"verao2026"}'

a senha nunca volta em nenhuma resposta e não pode ser lida depois. ela é gravada como um hash com salt (scrypt), e o que a API devolve é só has_password. se você precisa dela depois, guarde-a você mesmo — é a mesma propriedade dos tokens de API. mande null para remover.

o que ela protege, e o que não: um link curto é público, então a senha controla o acesso ao destino, não o esconde de quem já a tem — quem souber a senha pode repassá-la. use para conteúdo que não deve circular por acidente, não para segredo de verdade.

tentativas erradas são limitadas por endereço e por link. o clique só é contado depois que a senha passa — contar quem não entrou não responderia nada.

prévia ao compartilhar

normalmente o card que aparece no WhatsApp ou no LinkedIn vem do destino: o link curto responde 301, o rastreador segue e lê as tags de lá. definir og_title, og_description ou og_image muda isso.

curl -X PATCH https://api.arvore.link/links/a1b2c3d4 \
  -H "Authorization: Bearer arv_..." \
  -H "Content-Type: application/json" \
  -d '{"og_title":"A promoção acaba domingo",
       "og_description":"frete grátis em tudo",
       "og_image":"https://exemplo.com.br/card.png"}'

com qualquer um desses campos definido, o link passa a responder HTML com essas tags para rastreadores de redes sociais — Facebook, WhatsApp, X, LinkedIn, Slack, Telegram, Discord, iMessage. todo o resto continua recebendo 301, incluindo Google e Bing: um buscador que segue o redirecionamento credita o destino, e é isso que queremos.

é uma heurística baseada no User-Agent, então erra às vezes: um rastreador novo recebe o 301 e não mostra o card. o pior caso é um compartilhamento sem card, não um link quebrado.

og_image aceita qualquer URL http(s) — aponte para uma imagem que você já hospeda. não existe upload de arquivo na API pública, pelo mesmo motivo que upload de avatar também não: é um fluxo de painel, não de script. sem imagem mas com título, uma é gerada a partir dele.

um link expirado ignora o card: mostrar prévia de um link morto é pior do que não mostrar nenhuma. num link com senha, o rastreador vê o card e a pessoa vê o formulário — o card só contém o que você escreveu nele, então isso é justamente o motivo de usar os dois juntos.

denúncias

POST /reports sinaliza um link curto para análise. está na API de propósito: quem monitora abuso, protege uma marca ou mantém um feed de ameaças reporta em volume, e obrigar essa pessoa a engenharia reversa seria perverso.

curl -X POST https://api.arvore.link/reports \
  -H "Content-Type: application/json" \
  -d '{"url":"https://arvr.ink/a1b2","reason":"phishing",
       "detail":"chegou por sms se passando pelo banco"}'

uma denúncia não desativa nada. ela grava um registro que uma pessoa analisa. se bastasse denunciar para derrubar, qualquer um teria um botão para tirar o link do concorrente do ar — e abusar do sistema de denúncias seria mais fácil do que o abuso que ele previne.

não precisa de token, e um token não ajuda. quem recebeu um link de phishing não tem conta aqui. denúncias são julgadas pelo conteúdo, então quem se identifica não tem mais peso do que quem não se identifica, e os limites valem para os dois.

url precisa ser um link do domínio de redirecionamento — qualquer outra coisa responde 400 NOT_A_SHORT_LINK em vez de ser gravada, senão a fila vira um depósito que alguém tem que garimpar. um slug que não existe responde 200 e não grava nada: um 404 transformaria o endpoint em um oráculo de slugs.

dois limites: 10 por hora por endereço e 20 por hora por link denunciado. o segundo existe porque mil denúncias do mesmo link enterrariam todas as outras — é a negação de serviço contra a análise, não contra o site.

erros

todo erro vem como {"error":"CODIGO"} com um status HTTP coerente. error é sempre texto, nunca um objeto — dá para checar e ramificar sem inspecionar a estrutura. os mais comuns: UNAUTHORIZED (401), SESSION_REQUIRED (403), SLUG_ALREADY_IN_USE (409), RATE_LIMITED (429).

um 400 de validação traz também fields, com o que está errado em cada entrada — útil para apontar o campo num formulário: {"error":"VALIDATION_FAILED","fields":{"url":["Invalid URL"]}}. quando o schema tem um código próprio, ele vem no lugar de VALIDATION_FAILED — por exemplo INVALID_TAG.

especificação

a referência abaixo é gerada a partir da especificação, então esta página não consegue divergir dela. aponte seu gerador de cliente para /openapi.json — ou /openapi.yaml se preferir ler.

o que não está aqui

a API cobre o que dá para automatizar. de fora ficam o login (é código por e-mail, não dá para roteirizar), a administração da conta — trocar e-mail, encerrar sessões, apagar a conta, criar tokens —, o envio de fotos e os contadores internos do site.

os contadores ficam de fora porque um estranho escrevendo nas nossas métricas não é uma funcionalidade. POST /reports é o contrário — ali o estranho escrevendo é a funcionalidade — então está documentado, mesmo sendo público e sem autenticação.

a maior parte disso não é escolha de documentação: com um token, esses endpoints respondem 403 de propósito. é o que faz a revogação de um token valer alguma coisa.

referência

base: https://api.arvore.link · versão 1.0.0

árvores

GET/linktreestoken

listar suas árvores

200 Linktrees401 Unauthorized

resposta 200 · Linktree

{
  "linktrees": [
    {
      "id": "k9fx2n1a",
      "slug": "meu-perfil",
      "title": "minha página",
      "bio": "links que importam",
      "avatar_url": "https://example.com/foto.jpg",
      "published": false,
      "created_at": 1739111400000,
      "updated_at": 1739111400000
    }
  ]
}
id
string
slug
string. Path on the redirect domain, 1-50 chars of [0-9a-z-]
title
string
bio
string, pode ser null
avatar_url
string, pode ser null
published
boolean. Unpublished árvores 404 on the public page
created_at
integer
updated_at
integer
POST/linktreestoken

criar uma árvore

slug
string, obrigatório. ex: meu-perfil
title
string, obrigatório
bio
string, opcional
avatar_url
string, opcional

201 Linktree created400 Validation error409 SLUG_ALREADY_IN_USE

resposta 201 · Linktree

{
  "id": "k9fx2n1a",
  "slug": "meu-perfil",
  "title": "minha página",
  "bio": "links que importam",
  "avatar_url": "https://example.com/foto.jpg",
  "published": false,
  "created_at": 1739111400000,
  "updated_at": 1739111400000
}
id
string
slug
string. Path on the redirect domain, 1-50 chars of [0-9a-z-]
title
string
bio
string, pode ser null
avatar_url
string, pode ser null
published
boolean. Unpublished árvores 404 on the public page
created_at
integer
updated_at
integer
GET/linktrees/{id}token

ver uma árvore com seus itens

id
path, obrigatório

200 Linktree with items401 Unauthorized404 Not found

resposta 200 · Linktree

{
  "id": "k9fx2n1a",
  "slug": "meu-perfil",
  "title": "minha página",
  "bio": "links que importam",
  "avatar_url": "https://example.com/foto.jpg",
  "published": false,
  "created_at": 1739111400000,
  "updated_at": 1739111400000,
  "items": [
    {
      "id": "p3qr7s2t",
      "title": "instagram",
      "url": "https://instagram.com/perfil",
      "short_url": "https://arvr.ink/x7y8",
      "position": 0,
      "active": true,
      "created_at": 1739111400000,
      "updated_at": 1739111400000
    }
  ]
}
id
string
slug
string. Path on the redirect domain, 1-50 chars of [0-9a-z-]
title
string
bio
string, pode ser null
avatar_url
string, pode ser null
published
boolean. Unpublished árvores 404 on the public page
created_at
integer
updated_at
integer
items
LinktreeItem[]
PATCH/linktrees/{id}token

editar o perfil, o slug ou publicar uma árvore

id
path, obrigatório
slug
string, opcional
title
string, opcional
bio
string, opcional
avatar_url
string, opcional
published
boolean, opcional

200 Linktree updated400 Validation error401 Unauthorized404 Not found409 SLUG_ALREADY_IN_USE

resposta 200 · Linktree

{
  "id": "k9fx2n1a",
  "slug": "meu-perfil",
  "title": "minha página",
  "bio": "links que importam",
  "avatar_url": "https://example.com/foto.jpg",
  "published": false,
  "created_at": 1739111400000,
  "updated_at": 1739111400000
}
id
string
slug
string. Path on the redirect domain, 1-50 chars of [0-9a-z-]
title
string
bio
string, pode ser null
avatar_url
string, pode ser null
published
boolean. Unpublished árvores 404 on the public page
created_at
integer
updated_at
integer
DELETE/linktrees/{id}token

apagar uma árvore

id
path, obrigatório

204 Deleted401 Unauthorized404 Not found

resposta 204 sem corpo.

GET/linktrees/{id}/analyticstoken

ver as visitas e os cliques de uma árvore

id
path, obrigatório
from
query. Start of time range (Unix timestamp in milliseconds, inclusive)
to
query. End of time range (Unix timestamp in milliseconds, inclusive)

200 Analytics data401 Unauthorized404 Not found

resposta 200 · LinktreeAnalytics

{
  "id": "k9fx2n1a",
  "slug": "meu-perfil",
  "title": "minha página",
  "published": true,
  "views": 128,
  "clicks": 41,
  "items": [
    {
      "id": "p3qr7s2t",
      "title": "instagram",
      "short_url": "https://arvr.ink/x7y8",
      "active": true,
      "count": 22
    }
  ]
}
id
string
slug
string
title
string
published
boolean
views
integer. Visits to the public page in the range
clicks
integer. Sum of item clicks in the range
items
object[]
POST/linktrees/{id}/itemstoken

adicionar um item na árvore

id
path, obrigatório
title
string, obrigatório. ex: instagram
url
string, obrigatório. ex: https://instagram.com/perfil
position
integer, opcional

201 Item created400 Validation error401 Unauthorized404 Linktree not found

resposta 201 · LinktreeItem

{
  "id": "p3qr7s2t",
  "title": "instagram",
  "url": "https://instagram.com/perfil",
  "short_url": "https://arvr.ink/x7y8",
  "position": 0,
  "active": true,
  "created_at": 1739111400000,
  "updated_at": 1739111400000
}
id
string
title
string
url
string. Final destination
short_url
string. Short link the public page points at, so clicks are counted
position
integer. Ascending order on the public page
active
boolean. Inactive items are hidden from the public page
created_at
integer
updated_at
integer
PATCH/linktrees/{id}/items/{itemId}token

editar um item da árvore

id
path, obrigatório
itemId
path, obrigatório
title
string, opcional
url
string, opcional
position
integer, opcional
active
boolean, opcional

200 Item updated400 Validation error401 Unauthorized404 Linktree or item not found

resposta 200 · LinktreeItem

{
  "id": "p3qr7s2t",
  "title": "instagram",
  "url": "https://instagram.com/perfil",
  "short_url": "https://arvr.ink/x7y8",
  "position": 0,
  "active": true,
  "created_at": 1739111400000,
  "updated_at": 1739111400000
}
id
string
title
string
url
string. Final destination
short_url
string. Short link the public page points at, so clicks are counted
position
integer. Ascending order on the public page
active
boolean. Inactive items are hidden from the public page
created_at
integer
updated_at
integer
DELETE/linktrees/{id}/items/{itemId}token

apagar um item da árvore

id
path, obrigatório
itemId
path, obrigatório

204 Deleted401 Unauthorized404 Linktree or item not found

resposta 204 sem corpo.

métricas

GET/analyticstoken

ver os cliques de todos os seus links

link_id
query
from
query. Start of time range (Unix timestamp in milliseconds, inclusive)
to
query. End of time range (Unix timestamp in milliseconds, inclusive)

200 Analytics data401 Unauthorized404 Link not found

resposta 200 · ClickEvent

{
  "count": 128,
  "clicks": [
    {
      "ts": "1771849585571#k9fx2n1a",
      "timestamp": 1771849585571,
      "country": "BR",
      "region": "SP",
      "region_name": "São Paulo",
      "timezone": "America/Sao_Paulo",
      "lat": -23.673,
      "lng": -46.5302,
      "device": "desktop",
      "platform": "macOS",
      "browser": "Chrome 145",
      "referer": "https://twitter.com",
      "link_id": "a1b2c3d4"
    }
  ]
}
ts
string. Sort key in the format "{timestamp_ms}#{random}"
timestamp
integer. Unix timestamp in milliseconds
country
string, pode ser null
region
string, pode ser null
region_name
string, pode ser null
timezone
string, pode ser null
lat
number, pode ser null
lng
number, pode ser null
device
string
platform
string, pode ser null
browser
string, pode ser null
referer
string, pode ser null
link_id
string

conta

GET/accounttoken

descobrir de quem é o token e qual workspace ele acessa

200 Account401 Unauthorized

resposta 200 · Account

{
  "user": {
    "id": "a1b2c3d4",
    "email": "voce@exemplo.com",
    "name": "Rafael",
    "avatar_url": "https://assets.arvore.link/avatars/w1/user-a1b2c3d4-k9fx2n1a.webp",
    "created_at": 1739111400000
  },
  "workspace": {
    "id": "w1x2y3z4",
    "name": "Pessoal"
  }
}
user
object
workspace
object

denúncias

POST/reportspúblico

denunciar um link curto

url
string, obrigatório. ex: https://arvr.ink/a1b2
reason
string, obrigatório. ex: phishing
detail
string, opcional. ex: Recebi por SMS dizendo que era o banco
email
string, opcional. ex: quem@reportou.com

200 Report received, or silently discarded for an unknown slug400 NOT_A_SHORT_LINK or a validation error429

resposta 200

{
  "ok": true
}
ok
boolean