Elido
12 min de leituraRecursos

API de encurtador de URLs: um guia rápido de 30 minutos em cinco linguagens

Do zero a uma automação funcional de links curtos em TypeScript, Python, Go, Ruby e PHP — autenticação, idempotência, tratamento de erros e as armadilhas que surgem apenas em produção

Marius Voß
DevRel · edge infra
Five-language quickstart diagram with code panels for TypeScript, Python, Go, Ruby, and PHP all pointing at a central Elido API endpoint

Uma API de encurtador de URLs é uma das integrações menores no backlog de uma equipa de engenharia típica. Três endpoints, um cabeçalho de autenticação, um payload JSON. A página de documentação promete a primeira chamada em cinco minutos. Então o tráfego de produção chega, a lógica de repetição cria links duplicados, o dashboard fica cheio de variantes /foo-1, /foo-2, /foo-3 do mesmo destino, e alguém abre um ticket.

Este post percorre a integração real. Autenticação, a primeira chamada, os quatro endpoints que cobrem a maioria dos casos de uso, idempotência, tratamento de erros, limites de taxa e as armadilhas de produção que o guia rápido de cinco minutos ignora. Exemplos de código em TypeScript, Python, Go, Ruby e PHP — os três primeiros através dos SDKs oficiais (@elido/sdk, elido-python, github.com/elido/elido-go), os dois últimos através de clientes HTTP comuns.

Pré-requisitos#

Faça login no dashboard, navegue até /settings/api e crie um token de acesso pessoal. Os tokens têm escopo de workspace — um token emitido no workspace A não pode criar links no workspace B. Tokens de conta de serviço (para sistemas de CI, ferramentas internas, integração máquina a máquina) são criados na mesma tela nos planos Pro e superiores; eles têm escopos explícitos (links:write, analytics:read, domains:write) e rotacionam independentemente dos tokens pessoais.

A URL base é https://api.elido.app/v1. Os domínios de redirecionamento (f.elido.me, s.elido.me, b.elido.me) são separados da superfície da API. Seus links curtos resolvem no domínio de redirecionamento; a API serve para criá-los, modificá-los e lê-los.

A especificação OpenAPI é publicada em https://api.elido.app/v1/openapi.json e está em conformidade com OpenAPI 3.1. Os SDKs oficiais são gerados a partir dessa especificação e republicados a cada lançamento da API; você também pode gerar seu próprio cliente em qualquer linguagem compatível com OpenAPI.

A primeira chamada#

Crie um link curto a partir da URL de destino. Cinco linhas em TypeScript:

import { Elido } from "@elido/sdk";

const elido = new Elido({ token: process.env.ELIDO_TOKEN! });

const link = await elido.links.create({
  destinationUrl: "https://shop.example.com/spring-sale",
});

console.log(link.shortUrl); // https://s.elido.me/abc123

Python:

from elido import Elido

client = Elido(token=os.environ["ELIDO_TOKEN"])

link = client.links.create(
    destination_url="https://shop.example.com/spring-sale",
)

print(link.short_url)  # https://s.elido.me/abc123

Go:

import "github.com/elido/elido-go/v2/elido"

client := elido.NewClient(elido.WithToken(os.Getenv("ELIDO_TOKEN")))

link, err := client.Links.Create(ctx, &elido.LinkCreateInput{
    DestinationURL: "https://shop.example.com/spring-sale",
})
if err != nil {
    return fmt.Errorf("create link: %w", err)
}

fmt.Println(link.ShortURL)

Ruby (sem SDK oficial — utilizando net/http):

require "net/http"
require "json"

uri = URI("https://api.elido.app/v1/links")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV['ELIDO_TOKEN']}"
req["Content-Type"] = "application/json"
req.body = { destination_url: "https://shop.example.com/spring-sale" }.to_json

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
link = JSON.parse(res.body)
puts link["short_url"]

PHP (Guzzle):

$client = new GuzzleHttp\Client(['base_uri' => 'https://api.elido.app/v1/']);

$res = $client->post('links', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('ELIDO_TOKEN')],
    'json'    => ['destination_url' => 'https://shop.example.com/spring-sale'],
]);

$link = json_decode((string) $res->getBody(), true);
echo $link['short_url'];

Todos os cinco produzem o mesmo resultado. O corpo da resposta contém a URL curta, o ID canônico do link, o ID do workspace e o timestamp de criação. O slug — abc123 no exemplo acima — é gerado pelo servidor, a menos que você passe custom_slug na requisição. O alfabeto do slug é base62 ([0-9A-Za-z]); o comprimento padrão é de seis caracteres.

Os quatro endpoints que usará realmente#

A API tem mais de quatro endpoints, mas a maioria das integrações permanece dentro deste conjunto.

POST /v1/links aceita a URL de destino e campos opcionais:

  • custom_slug — um slug à sua escolha (deve ser único dentro do workspace).
  • domain_id — para links de domínio personalizado; o domínio principal do workspace é usado se omitido.
  • tags — um array de strings de formato livre para organização.
  • utm — parâmetros de campanha para anexar ao destino no momento do redirecionamento.
  • expires_at — timestamp ISO 8601 após o qual o link retorna 410 Gone.
  • password — se definido, o redirecionamento serve uma página de password antes de reencaminhar.
  • metadata — objeto JSON opaco que o redirecionamento não interpreta; útil para as suas próprias chaves de junção.

O slug personalizado é o campo que costuma causar problemas às equipas em produção. Se passar um slug que já está em uso por outro link no mesmo workspace, a API retorna 409 Conflict. O tratador de repetição ingénuo que anexa um contador (my-slug-1, my-slug-2) produz o problema de links duplicados descrito no início. O comportamento de repetição correto é descrito na secção de idempotência abaixo.

GET /v1/links/{id} retorna o registo completo do link, incluindo a contagem atual de cliques, o timestamp do clique mais recente e toda a configuração. O ID do link é o identificador canônico — os slugs podem mudar (o plano Pro+ suporta renomeação de slugs), os IDs não.

GET /v1/links?domain_id=…&tag=…&limit=… lista links no workspace com filtros. A paginação é baseada em cursor; o next_cursor na resposta é opaco e volta como o parâmetro de consulta cursor na próxima requisição.

PATCH /v1/links/{id} aceita os mesmos campos que a criação. As atualizações mais comuns: alterar a URL de destino (útil para rotação de campanha sem reimprimir QR codes), alterar tags, estender expires_at. Atualizar o slug é um endpoint separado POST /v1/links/{id}/rename que lida com o redirecionamento 301 do slug antigo por um período de retenção configurável (padrão de 30 dias).

DELETE /v1/links/{id} realiza uma exclusão lógica (soft-delete). O link retorna 410 Gone pelos próximos 90 dias e, em seguida, é eliminado permanentemente. A visualização da lixeira do dashboard mostra links excluídos logicamente; pode restaurar via dashboard ou via POST /v1/links/{id}/restore dentro da janela de 90 dias.

Chaves de idempotência#

Toda requisição de mutação — POST, PATCH, DELETE — aceita um cabeçalho Idempotency-Key. O valor do cabeçalho é uma string opaca de até 255 caracteres; o servidor armazena o corpo da resposta e o código de status por 24 horas indexados por (workspace_id, idempotency_key) e retorna a resposta armazenada se a mesma chave for apresentada novamente.

Os SDKs oficiais geram chaves de idempotência automaticamente quando não são fornecidas. Pode substituir:

const link = await elido.links.create(
  { destinationUrl: "https://shop.example.com/spring-sale" },
  { idempotencyKey: "order-12345-link" },
);

O caso de uso é um loop de repetição. Se o seu job cria um link como parte do processamento de um pedido upstream, gere a chave de idempotência a partir do ID do pedido. Uma repetição do mesmo job vê a mesma chave, atinge o cache de idempotência e retorna o link criado originalmente, em vez de produzir um segundo link.

A principal armadilha: o cache de idempotência dura 24 horas, não para sempre. Uma repetição no terceiro dia de um job travado criará um novo link. Se a integração for executada em lotes de vários dias, armazene o ID do link retornado pela primeira criação bem-sucedida e consulte-o antes de emitir novamente.

Uma segunda armadilha: a idempotência é por workspace. A mesma chave em dois workspaces cria dois links. Esta é a semântica correta para uma API multi-workspace, mas pode surpreender equipas que assumem que a chave é globalmente única.

Tratamento de erros#

A API retorna códigos de status HTTP padrão, além de um corpo de erro estruturado:

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Workspace rate limit of 100 req/s exceeded. Retry after 1 second.",
    "request_id": "req_01HXYZAB123",
    "retry_after": 1
  }
}

Os códigos que verá com mais frequência:

  • 400 invalid_request — falha na validação do payload. O campo message lista os campos específicos. Não repita a chamada; corrija o payload.
  • 401 unauthorized — token ausente ou inválido. Não repita sem rotacionar o token.
  • 403 forbidden — o token não tem o escopo necessário. Verifique a lista de escopos do token em /settings/api.
  • 404 not_found — o recurso não existe ou o token não tem acesso a ele (retornamos 404 em vez de 403 para evitar fugas de informação sobre a existência de recursos a chamadores não autorizados).
  • 409 conflict — slug já em uso, ou edição simultânea detetada (PATCH numa versão antiga). Procure novamente e tente de novo.
  • 429 rate_limit_exceeded — aguarde de acordo com o valor de retry_after.
  • 500 internal_server_error — falha no lado do servidor. Seguro repetir com a mesma chave de idempotência.
  • 502 bad_gateway, 503 service_unavailable, 504 gateway_timeout — problemas transitórios de infraestrutura. Aguarde e tente novamente.

Os SDKs oficiais implementam backoff exponencial com jitter para 429, 500, 502, 503 e 504. Eles não repetem 400, 401, 403, 404 ou 409 — esses são erros de programação ou conflitos de lógica de negócio, não falhas transitórias. Clientes HTTP personalizados devem seguir o mesmo padrão; repetir um 400 com o mesmo payload não produzirá um resultado diferente.

O request_id no corpo do erro é o campo a ser incluído nos tickets de suporte. Podemos rastrear qualquer requisição a partir desse ID através do log de auditoria, do log da aplicação e das métricas da plataforma — e não podemos rastrear uma requisição sem ele.

Limites de taxa#

Os limites de taxa publicados são 100 requisições por segundo por workspace no plano Pro, 500 no Business e um limite negociado no Enterprise. O nível gratuito é de 10 req/s.

O estado do limite de taxa é exposto em três cabeçalhos de resposta em cada resposta da API:

  • X-RateLimit-Limit — o limite atual por segundo.
  • X-RateLimit-Remaining — requisições restantes no segundo atual.
  • X-RateLimit-Reset — timestamp Unix de quando o balde (bucket) faz reset.

O limite de 100/s é uma implementação de token-bucket com uma capacidade de burst de 200 — o que significa que pode emitir 200 requisições de uma vez se o bucket estiver cheio e, em seguida, estabilizar na taxa sustentada de 100/s. A maioria dos jobs de criação de links curtos cabe confortavelmente no burst; integrações ricas em análise que paginam eventos históricos de cliques beneficiam da margem do plano Pro.

Para operações em massa no Business+, o endpoint POST /v1/links/bulk aceita até 1000 links por requisição e conta como uma unidade de limite de taxa. Este é o endpoint correto para qualquer job que cria mais de cem links de uma vez.

O que os SDKs fazem que o HTTP comum não faz#

Os SDKs oficiais oferecem quatro coisas que se pagam rapidamente:

  • Repetição automática com backoff para os códigos de status repetíveis.
  • Geração de chave de idempotência quando não fornecida explicitamente.
  • Erros tipados para que possa fazer catch (err) { if (err instanceof ElidoRateLimitError) { … } } em vez de analisar JSON em blocos catch.
  • Iteradores de paginação para que os endpoints de listagem exponham iteradores assíncronos ou geradores em vez de exigir tratamento manual de cursores.

O SDK de Go expõe adicionalmente o cliente HTTP subjacente para instrumentação — útil se quiser ligá-lo à sua configuração de rastreamento existente. A página de recursos de API + SDKs do repositório cobre toda a superfície; a referência da API é publicada em /docs/api-reference.

Acesso a análises#

Os endpoints de análise são somente leitura e residem em /v1/workspaces/{id}/analytics/. As consultas mais comuns:

  • GET .../links/{id}/clicks?from=…&to=… — eventos de cliques brutos com paginação. Útil para pipelines de exportação.
  • GET .../timeseries?from=…&to=…&bucket=day — contagem de cliques agrupados por dia para um intervalo de tempo.
  • GET .../breakdown/country?from=…&to=… — detalhamento geográfico.
  • GET .../breakdown/referrer?from=…&to=… — detalhamento por referenciador (referrer).

O feed de eventos de cliques brutos é o maior. Um workspace com 10M de cliques por mês produz cerca de 600MB de JSON por mês de dados de eventos brutos. Para exportações nesta escala, o guia de exportação para ClickHouse cobre o mecanismo de exportação em massa que ignora o envelope JSON e faz o streaming diretamente do warehouse de análise.

Webhooks para eventos de clique#

Webhooks são o inverso do polling — em vez de perguntar à API por novos cliques, a API entrega-os ao seu endpoint. Configure em /settings/webhooks:

await elido.webhooks.create({
  url: "https://your-app.example/webhooks/elido",
  events: ["link.click", "link.created", "link.expired"],
  secret: process.env.WEBHOOK_SIGNING_SECRET,
});

Cada entrega inclui um cabeçalho Elido-Signature contendo um HMAC-SHA256 do corpo da requisição com o seu segredo compartilhado. Verifique a assinatura antes de processar — sem ela, qualquer chamador pode postar no seu endpoint de webhook e fazer-se passar pela Elido.

A semântica de entrega é pelo menos uma vez (at-least-once) com backoff exponencial até uma retenção máxima de 72 horas. Para o formato detalhado e o comportamento de repetição, o post webhooks vs polling para rastreamento de cliques compara os dois padrões de integração.

Um exemplo prático: automação de campanha#

A integração que motiva a maior parte da adoção da API assemelha-se a isto. A sua automação de marketing cria uma campanha no Customer.io ou HubSpot. Um hook é acionado quando a campanha é publicada. O seu handler cria o link curto, anexa-o ao registo da campanha e envia-o de volta para a ferramenta de gestão de campanha para substituir no template de e-mail.

Em TypeScript:

import { Elido } from "@elido/sdk";

const elido = new Elido({ token: process.env.ELIDO_TOKEN! });

export async function onCampaignPublished(campaign: Campaign) {
  const link = await elido.links.create(
    {
      destinationUrl: campaign.destinationUrl,
      tags: ["campaign", `campaign:${campaign.id}`, campaign.channel],
      utm: {
        source: campaign.channel,
        medium: "email",
        campaign: campaign.slug,
      },
      metadata: { campaign_id: campaign.id, batch: campaign.batchId },
    },
    {
      idempotencyKey: `campaign-${campaign.id}-link`,
    },
  );

  await campaignStore.update(campaign.id, { shortUrl: link.shortUrl });
  return link;
}

A chave de idempotência é derivada do ID da campanha. Se o hook de campanha publicada for acionado duas vezes (e ele é — as entregas de webhook são pelo menos uma vez), a segunda chamada retorna o mesmo link sem criar uma duplicata. O campo metadata guarda as suas próprias chaves de junção para que possa correlacionar os eventos de clique da Elido de volta à campanha sem analisar tags.

Para atribuição de campanha de ponta a ponta com templates UTM e encaminhamento de conversão, o artigo principal sobre rastreamento UTM percorre todo o pipeline.

O que ainda não está na API#

Duas coisas frequentemente solicitadas, mas atualmente não disponíveis:

  • Um GET de análise de link único que retorna todos os detalhamentos numa única chamada. O modelo atual exige chamadas separadas para cliques, país, referenciador, dispositivo e séries temporais. A agregação está no roteiro (roadmap); por enquanto, os SDKs paralelizam as requisições com um único método auxiliar.
  • Replay de webhooks a partir da API. O dashboard expõe o histórico de entrega de webhooks e suporta o replay; a API ainda não. Isto também está no roteiro.

Se um recurso estiver na especificação OpenAPI, ele é suportado. Se estiver neste post, mas não na especificação, trate-o como planeado, em vez de garantido.

Leitura relacionada#

Experimente o Elido

Encurtador de URL hospedado na UE: domínios personalizados, análises profundas e API aberta. Plano gratuito — sem cartão de crédito.

Tags
url shortener api
bitly api alternative
link shortener api
rest api short link
url shortener sdk
openapi 3.1
idempotency keys

Continuar lendo