33 min de leituraRecursos

API de encurtador de URL: encurtar uma URL em Python, JS e Go

Encurte uma URL com uma chamada de API em Python, JavaScript, Go, Java, C#, PHP, Ruby ou PowerShell, com retentativas idempotentes e limites de lote.

Marius Voß
DevRel · edge infra

Para encurtar uma URL com uma API, envie um único POST autenticado: coloque a URL longa em um corpo JSON como destination_url, mande para https://api.elido.app/v1/links com Authorization: Bearer <your key> e leia short_url na resposta 201 Created. Essa é a integração inteira, em qualquer linguagem. As oito linguagens abaixo só diferem no jeito de escrever a requisição. E no detalhe de produção que morde primeiro.

A primeira chamada leva cinco minutos. O problema começa depois, quando o tráfego real chega e um tratador de retentativas cria duplicatas: o painel se enche de variações /foo-1, /foo-2 e /foo-3 do mesmo destino, e alguém abre um chamado. Este guia é o passo a passo da API de encurtador de URL que eu gostaria que aquele time tivesse lido antes: autenticação, primeira chamada, os quatro endpoints de links, chaves de idempotência, códigos de erro, limites de taxa e, depois, a mesma requisição de link curto em oito linguagens, com o único hábito de que cada ambiente de execução precisa.

Ainda escolhendo o serviço? A comparação de APIs de encurtador de URL gratuitas coloca lado a lado os formatos de requisição e os limites dos planos gratuitos entre fornecedores. Tudo abaixo se refere à API REST da Elido.

Vá direto para a sua linguagem:

  • Python: requests para um link, httpx com um semáforo para milhares.
  • JavaScript e Node.js: fetch, por que a chave fica fora do navegador, um pool de workers em vez de Promise.all.
  • Go: um http.Client compartilhado com timeout, errgroup.SetLimit para lotes.
  • Java: o HttpClient do JDK, threads virtuais atrás de um Semaphore.
  • C# e .NET: PostAsJsonAsync e depois um cliente tipado do IHttpClientFactory.
  • PHP, Laravel e WordPress: curl, Http:: com retentativas, wp_remote_post na publicação.
  • Ruby: Net::HTTP.post e depois Faraday com um middleware de retentativa que de fato repete um POST.
  • PowerShell: Invoke-RestMethod, uma chave em um cofre e uma coluna inteira de CSV.

Pré-requisitos: uma chave de API e uma URL base

Entre no painel, abra /dashboard/api-keys e crie uma chave de API. As chaves começam com elido_ e aparecem uma única vez. Cada chave é ligada a um workspace: uma chave emitida no workspace A não consegue criar links no workspace B, e a API descobre a qual workspace você se refere só pela chave. Tokens de usuário máquina (para sistemas de CI, ferramentas internas e integrações máquina a máquina) ficam em /dashboard/machine-users e são rotacionados de forma independente das chaves pessoais.

Os dois tipos carregam um papel de workspace predefinido (viewer, editor ou admin) em vez de escopos por endpoint, então um job de CI que só cria links recebe editor. O guia de permissões de chaves de API lista o que cada papel alcança. Mudanças em webhooks exigem admin, e ele explica o motivo.

A página de chaves de API da Elido listando três chaves com seus prefixos, papéis editor e viewer, datas de último uso e contagens de requisições das últimas 24 horas

A URL base é https://api.elido.app, e as rotas de links ficam sob /v1. Os domínios de redirecionamento (f.elido.me no Free, s.elido.me nos planos pagos, b.elido.me no Business) são uma superfície separada: seus links curtos são resolvidos lá, enquanto a API os cria e os edita. A especificação OpenAPI 3.1 por trás da referência da API é o contrato. Se um campo está na especificação, ele tem suporte.

Uma pegadinha atinge contas novas. Escritas feitas com uma chave cujo dono não verificou o e-mail voltam como 403 (email_not_verified). As leituras continuam funcionando. Tokens de usuário máquina são isentos. Verifique o endereço uma vez e o erro some.

Esta é a requisição que cada seção de linguagem abaixo reproduz, em curl:

curl -sS https://api.elido.app/v1/links \
  -H "Authorization: Bearer $ELIDO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"destination_url": "https://shop.example.com/spring-sale"}'

A resposta é um 201 Created com o registro do link. Reduzido aos campos que você vai usar:

{
  "id": 48213,
  "workspace_id": 42,
  "domain_id": 3,
  "slug": "aB3xK9",
  "destination_url": "https://shop.example.com/spring-sale",
  "tags": [],
  "redirect_status": 302,
  "password_set": false,
  "short_url": "https://s.elido.me/aB3xK9",
  "created_at": "2026-11-11T09:14:03Z"
}

Guarde o id se for editar, expirar ou excluir o link um dia: é o identificador canônico, e slugs podem mudar enquanto IDs não. Você não passou domain_id, então a API usou o domínio curto padrão do seu plano. O slug é gerado pelo servidor, a menos que você envie slug; slugs gerados são base62 ([0-9A-Za-z]) e têm no máximo seis caracteres.

Mantenha a chave em uma variável de ambiente (ELIDO_API_KEY em todos os exemplos aqui), nunca no código-fonte: a chave de um encurtador é uma credencial que gasta cota, e quem a tiver pode criar links na sua, o que pesa mais em JavaScript, como a seção de Node explica.

Os quatro endpoints que você vai realmente usar

A API tem mais de quatro endpoints, mas a maioria das integrações fica dentro deste conjunto. As rotas planas /v1/links inferem o workspace pela sua chave; os caminhos canônicos em /v1/workspaces/{workspace_id}/links se comportam da mesma forma e exigem o ID explícito.

Diagrama de centro e raios dos quatro endpoints centrais de links em torno do recurso /v1/links: POST cria, GET lê, PATCH atualiza e DELETE exclui, cada um com sua principal pegadinha.

POST /v1/links recebe destination_url mais campos opcionais:

  • slug: um final de link escolhido por você; precisa ser único no domínio.
  • domain_id: para um domínio personalizado; omita para usar o domínio curto padrão do seu plano.
  • title e tags: um rótulo do painel e strings livres para filtrar depois.
  • expires_at: um carimbo de data e hora RFC 3339 depois do qual o link para de redirecionar e mostra uma página de expirado.
  • redirect_status: 301, 302 (o padrão) ou 307.

Senhas não são definidas na criação: envie um PATCH logo em seguida, e o redirecionamento mostrará uma página de senha antes de encaminhar. Parâmetros UTM vão direto em destination_url, e suas próprias chaves de junção em tags.

O slug personalizado é o campo que morde times em produção. Reutilize um slug no mesmo domínio e você recebe 409. Um tratador ingênuo que acrescenta um contador (my-slug-1, my-slug-2) é exatamente como começa a bagunça de duplicatas do início. A correção é uma chave de idempotência, tratada duas seções abaixo.

GET /v1/links/{id} retorna o registro completo, incluindo short_url. Contagens de cliques não estão no registro do link. Elas vêm dos endpoints de analytics descritos no guia da API de analytics de cliques.

GET /v1/links?limit=…&tags=…&host=… lista links com filtros (q pesquisa, status e folder refinam). A paginação é por cursor: devolva o next_cursor da resposta como cursor e pare quando for null.

PATCH /v1/links/{id} aceita os campos de criação. As atualizações comuns são um novo destination_url (rotação de campanha sem reimprimir um QR code), novas tags e um expires_at mais tardio. Enviar um novo slug renomeia o link. O slug antigo morre na hora. Não renomeie nada que já esteja impresso.

DELETE /v1/links/{id} retorna 204 No Content e move o link para a lixeira: ele para de redirecionar e some das chamadas de listagem e leitura. Ainda não foi embora. POST /v1/links/{id}/restore o traz de volta e GET /v1/links/trash mostra o que há lá. Depois de 30 dias a lixeira é esvaziada de vez.

Todo POST, PUT e PATCH aceita um cabeçalho Idempotency-Key, uma string opaca escolhida por você. Na primeira vez que uma chave chega com uma resposta bem-sucedida, o servidor guarda o código de status e o corpo por 24 horas, no escopo de (workspace, key). Uma repetição da mesma chave com a mesma requisição recebe a resposta guardada, então uma retentativa devolve o link que já existe em vez de criar um segundo.

Três detalhes decidem se funciona para você:

  • Só respostas bem-sucedidas são guardadas. Um 429 ou um 500 não entra no cache, então a próxima retentativa com a mesma chave tem uma segunda tentativa de verdade.
  • O servidor tira a impressão digital do método, do caminho e do corpo. Reutilize uma chave com outro corpo e você recebe 422 com idempotency_key_mismatch, que é um bug de quem chama, não algo a repetir.
  • DELETE não aceita chave; excluir duas vezes já é idempotente por natureza.

A chave em si deve vir de algo estável. Derive-a do evento de negócio (order-12345-link) quando houver um, ou de um hash SHA-256 da URL de destino em um job em lote. Um UUID aleatório por tentativa anula o propósito. Um por URL funciona até o lote ser reexecutado.

Pipeline em que um hook de campanha de pelo menos uma entrega dispara duas chamadas de criação com a mesma chave de idempotência; o cache de 24 horas deduplica a segunda e exatamente um link é criado.

A janela de 24 horas é a pegadinha. Uma retentativa no terceiro dia de um job travado cria um link novo, então, em lotes de vários dias, guarde o id da primeira criação bem-sucedida e consulte-o antes de emitir de novo. As chaves também são por workspace: a mesma chave em dois workspaces cria dois links, o que está certo para uma API multi-workspace e ainda assim surpreende times que achavam que as chaves eram globais.

Uma chave baseada no hash do destino tem um efeito colateral que vale conhecer. Duas campanhas apontando para a mesma URL em 24 horas compartilham um link. Se você quer dois links, ponha o ID da campanha na chave.

Tratamento de erros e códigos de status

Os erros chegam como um pequeno objeto JSON com um error legível e, em alguns casos, um code legível por máquina:

{ "error": "destination_url is required" }

Os códigos de status que você vai encontrar:

  • 400: falha de validação, como um destination_url ausente, um expires_at malformado ou um redirect_status inválido. Corrija o payload; não repita.
  • 401: a chave está ausente, revogada ou digitada errado. Confira se o cabeçalho diz Bearer com um espaço antes da chave.
  • 402: a cota de links do seu plano acabou. Repetir não ajuda; fazer upgrade ou esperar o próximo período, sim.
  • 403: o papel da chave não permite a ação (uma chave viewer não cria links), o e-mail da conta não foi verificado ou o destino foi bloqueado pela varredura de segurança ("code": "destination_blocked").
  • 404: o link não existe ou esta chave não consegue vê-lo. Retornamos 404 em vez de 403 para que quem chama não possa sondar recursos que não são seus.
  • 409: o slug já está em uso nesse domínio.
  • 422: uma chave de idempotência reutilizada com outro corpo, ou uma combinação de opções não permitida.
  • 429: você atingiu o limite de taxa. Espere o Retry-After e tente de novo com a mesma chave de idempotência.
  • 500, 502, 503, 504: uma falha do servidor ou transitória. Espere com backoff e repita com a mesma chave.
Divisão de decisão que separa os códigos de status da API em uma coluna de repetir com backoff (429, 500, 502, 503, 504) e outra de não repetir (400, 401, 403, 404, 409) para erros de programação e de conflito.

Essa divisão é toda a política de retentativa, e cada seção de linguagem abaixo a implementa do mesmo jeito: repita 429 e 5xx, desista de todo o resto. Repetir um 400 com o mesmo payload não pode dar outra resposta, e repetir um 401 três vezes é só uma falha mais lenta. O guia de limites de taxa e retentativas aprofunda o jitter e o motivo de laços de retentativa cegos transformarem uma queda em duas.

Limites de taxa e o endpoint de lote

O tráfego da API é medido por workspace com um token bucket:

PlanoTaxa sustentadaRajada
Free10 req/s20
Pro30 req/s60
Business100 req/s200

Um balde cheio deixa você enviar a rajada de uma vez. Depois você se acomoda na taxa sustentada. Cada chave de API também tem seu próprio balde além do do workspace, então um único script descontrolado não consegue gastar sozinho toda a cota do workspace.

Uma requisição rejeitada recebe 429, um cabeçalho Retry-After: 1 e um cabeçalho X-RateLimit-Scope que diz qual balde esvaziou (workspace ou api_key). Não há cabeçalhos de cota restante para se guiar. Então a abordagem prática é um teto fixo de concorrência mais uma retentativa em 429: oito requisições em andamento é um bom começo no Pro, e quatro servem para o Free. Uma peculiaridade me pegou. O corpo do 429 é um texto em formato JSON enviado como text/plain, então um cliente que só analisa tipos de conteúdo JSON verá uma string simples onde esperava um objeto.

Trabalho grande? POST /v1/links/bulk recebe {"links": [...]} com até 100 itens e custa uma requisição no limite. Ele responde 200 mesmo quando alguns itens falham, com contagens ok e failed e um status por item de ok ou error, então leia cada item. Não pule isso. Os links devolvidos trazem um slug, mas não short_url, o que significa que você monta a URL com seu domínio e o slug. O passo a passo de importação em massa do Google Sheets move uma planilha inteira assim.

Encurte uma URL na sua linguagem: a forma comum

Cada seção daqui em diante envia a requisição da primeira chamada e aplica os mesmos quatro hábitos: a chave vinda do ambiente, um timeout em toda requisição, o código de status conferido antes de analisar o corpo e uma Idempotency-Key estável em tudo que possa rodar duas vezes. Não vou reexplicá-los a cada linguagem. O que muda é o idioma. Cada ambiente de execução também tem um detalhe que as pessoas erram.

Cada seção abre com a chamada mínima que funciona. Isso basta para um script rodado à mão. Depois vem a peça de produção que mais perguntam naquele ambiente: concorrência em Python, Go ou Java; vida útil do cliente em C#; hospedagem em PHP; middleware em Ruby; segredos e CSVs em PowerShell. Quer rodá-los como estão? Crie uma chave no plano gratuito, exporte como ELIDO_API_KEY e todo trecho funciona sem mudanças.

Encurtar uma URL em Python

Em Python, encurtar uma URL é um requests.post com a chave em um cabeçalho Bearer, o destino em json= e um timeout:

import os
import requests

resp = requests.post(
    "https://api.elido.app/v1/links",
    headers={"Authorization": f"Bearer {os.environ['ELIDO_API_KEY']}"},
    json={"destination_url": "https://example.com/a-very-long-path?with=params"},
    timeout=10,
)
resp.raise_for_status()
print(resp.json()["short_url"])  # https://s.elido.me/aB3xK9

A biblioteca requests não tem timeout padrão, então sem timeout=10 uma conexão travada bloqueia o script para sempre. raise_for_status() transforma um 401 ou um 500 em exceção, em vez de deixar uma chamada falha parecer sucesso. Se você não pode instalar nada com pip, o urllib.request da biblioteca padrão envia o mesmo POST com mais cerimônia.

Lote com httpx e um semáforo

Um laço sobre requests.post serve para algumas centenas de links. Para dezenas de milhares, o tempo vai embora esperando a rede, e o httpx com asyncio sobrepõe essas esperas. O semáforo é o que mais importa. Ele limita as requisições em andamento, e essa é a diferença entre um lote rápido e uma parede de 429 de onde o laço de retentativa tem de se desenterrar depois.

import asyncio
import hashlib
import os

import httpx

API = "https://api.elido.app/v1/links"


async def shorten_all(urls: list[str], concurrency: int = 8) -> dict[str, str]:
    gate = asyncio.Semaphore(concurrency)
    headers = {"Authorization": f"Bearer {os.environ['ELIDO_API_KEY']}"}

    async with httpx.AsyncClient(headers=headers, timeout=10) as client:

        async def one(url: str) -> tuple[str, str]:
            key = hashlib.sha256(url.encode()).hexdigest()  # same key on every retry
            async with gate:
                for attempt in range(3):
                    r = await client.post(
                        API,
                        json={"destination_url": url},
                        headers={"Idempotency-Key": key},
                    )
                    if r.status_code == 429 or r.status_code >= 500:
                        await asyncio.sleep(int(r.headers.get("Retry-After", 2**attempt)))
                        continue
                    if r.is_success:
                        return url, r.json()["short_url"]
                    return url, f"ERROR {r.status_code}: {r.text}"
                return url, "ERROR: gave up after 3 attempts"

        pairs = await asyncio.gather(*(one(u) for u in urls))
    return dict(pairs)


# results = asyncio.run(shorten_all(my_urls))

Os resultados são indexados pela URL original. Uma falha parcial continua visível. Como a chave é um hash do destino, reexecutar a lista inteira depois de corrigir três linhas ruins não cria nada duas vezes. Dormir dentro do semáforo é proposital: um worker mandado esperar não deve passar o lugar dele a uma requisição nova.

Encurtar uma URL em JavaScript e Node.js

Em JavaScript, encurtar uma URL é um POST com fetch. O Node traz fetch como global desde a versão 18, então não há nada a instalar:

const res = await fetch("https://api.elido.app/v1/links", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ELIDO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ destination_url: "https://example.com/spring-sale" }),
  signal: AbortSignal.timeout(10_000),
});

if (!res.ok)
  throw new Error(`shorten failed: ${res.status} ${await res.text()}`);

const { short_url } = await res.json();
console.log(short_url);

Rode com node --env-file=.env shorten.mjs. Aquela linha if (!res.ok) não é enfeite: fetch só rejeita em falha de rede ou aborto, então um 401 chega como uma promise resolvida com ok: false. Usuários de Axios se machucam nisso uma vez. Em geral aparece como um undefined onde devia haver um link.

Mantenha a chave no servidor

Cole esse trecho em um componente React e duas coisas quebram. O CORS bloqueia a resposta, porque uma API autenticada por chave não envia Access-Control-Allow-Origin para sites quaisquer. O problema maior está por trás do erro: uma chave no código do front-end pode ser lida por qualquer pessoa que abra o DevTools, e um prefixo NEXT_PUBLIC_ a anuncia. Então o navegador envia uma URL simples para a sua rota. A rota acrescenta a chave.

Uma chamada fetch do navegador a uma API de encurtador de URL bloqueada pelo CORS com a chave de API exposta no bundle, comparada ao caminho correto em que o navegador chama uma rota do servidor que guarda a chave e repassa a requisição

Valide a URL que a sua rota aceita, ou você construiu um redirecionamento aberto com passos extras; o texto sobre vulnerabilidades de redirecionamento aberto mostra o que atacantes fazem com um.

Para um lote, resista a Promise.all(urls.map(shorten)). Ele dispara todas as requisições no mesmo tick, então 500 URLs viram 500 conexões simultâneas e a maioria volta com 429. Um pequeno pool de workers puxando de uma fila compartilhada mantém a concorrência plana, por mais longa que a lista fique:

import { createHash } from "node:crypto";

async function shorten(url) {
  const key = createHash("sha256").update(url).digest("hex"); // stable across retries
  for (let attempt = 0; attempt < 3; attempt++) {
    const res = await fetch("https://api.elido.app/v1/links", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.ELIDO_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": key,
      },
      body: JSON.stringify({ destination_url: url }),
      signal: AbortSignal.timeout(10_000),
    });
    if (res.status === 429 || res.status >= 500) {
      const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt);
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue;
    }
    if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
    return (await res.json()).short_url;
  }
  throw new Error("gave up after 3 attempts");
}

export async function shortenAll(urls, concurrency = 8) {
  const queue = [...urls];
  const results = new Map();
  const worker = async () => {
    while (queue.length) {
      const url = queue.shift();
      try {
        results.set(url, await shorten(url));
      } catch (err) {
        results.set(url, `ERROR: ${err.message}`); // one bad URL must not sink the batch
      }
    }
  };
  await Promise.all(Array.from({ length: concurrency }, worker));
  return results;
}

Em runtimes edge (Vercel Edge, Cloudflare Workers, Deno), o node:crypto pode faltar; faça o hash com crypto.subtle.digest("SHA-256", …) e o resto não muda.

Encurtar uma URL em Go

Em Go, encurtar uma URL pede net/http e encoding/json da biblioteca padrão: serializar o corpo, definir dois cabeçalhos, decodificar short_url. Escreva desde o início como uma função sobre um cliente compartilhado, porque é a versão que você vai manter:

var client = &http.Client{Timeout: 10 * time.Second}

func shorten(ctx context.Context, destination string) (string, error) {
	body, err := json.Marshal(map[string]string{"destination_url": destination})
	if err != nil {
		return "", err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		"https://api.elido.app/v1/links", bytes.NewReader(body))
	if err != nil {
		return "", err
	}
	sum := sha256.Sum256([]byte(destination))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("ELIDO_API_KEY"))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Idempotency-Key", hex.EncodeToString(sum[:]))

	resp, err := client.Do(req)
	if err != nil {
		return "", fmt.Errorf("post: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 300 {
		msg, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
		return "", fmt.Errorf("shorten %s: %s %s", destination, resp.Status, msg)
	}
	var link struct {
		ShortURL string `json:"short_url"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&link); err != nil {
		return "", fmt.Errorf("decode: %w", err)
	}
	return link.ShortURL, nil
}

http.DefaultClient e http.Post não têm nenhum timeout. Nenhum. Um host que não responde pode segurar uma goroutine por minutos. Um único cliente no nível do pacote mantém o pool de conexões vivo entre as chamadas, e NewRequestWithContext faz uma CLI cancelada ou um chamador desconectado interromper a requisição em vez de deixá-la rodando. Se um lote abrir bem mais sockets do que o esperado, aumente MaxIdleConnsPerHost no transporte: o padrão de 2 é invisível em um script e caro quando oito goroutines dividem um host.

Fan-out limitado com errgroup.SetLimit

Uma goroutine por URL é barato para o Go e ruinoso para o limite de taxa: 5.000 URLs viram 5.000 requisições de uma vez. O errgroup com SetLimit dá um fan-out com teto.

Uma goroutine por URL inundando a API do encurtador com requisições limitadas por taxa, comparada a um errgroup com SetLimit mantendo em andamento um número limitado de requisições de encurtamento em lote
func shortenAll(ctx context.Context, urls []string) map[string]string {
	var (
		g   errgroup.Group
		mu  sync.Mutex
		out = make(map[string]string, len(urls))
	)
	g.SetLimit(8) // at most 8 requests in flight, whatever len(urls) is

	for _, u := range urls {
		g.Go(func() error {
			short, err := shorten(ctx, u)
			if err != nil {
				short = "ERROR: " + err.Error()
			}
			mu.Lock()
			out[u] = short
			mu.Unlock()
			return nil
		})
	}
	_ = g.Wait()
	return out
}

Isto usa um errgroup.Group simples de propósito. errgroup.WithContext cancela tudo no primeiro erro, o que serve para uma etapa de build tudo ou nada e é exatamente errado para um importador que deve encurtar o que puder, seguir adiante nas linhas ruins e relatar as falhas no fim. Desde o Go 1.22 a variável do laço é por iteração, então a velha linha u := u sumiu, mas o mutex não é opcional: go test -race vai sinalizar um mapa sem proteção. Acrescente uma retentativa em 429 e 5xx dentro de shorten usando select em ctx.Done() e time.After, para o backoff continuar interrompível.

Encurtar uma URL em Java

Em Java, encurtar uma URL é um POST pelo java.net.http.HttpClient, que está no JDK desde o Java 11, então a primeira versão não precisa de nenhuma dependência do Maven:

private static final HttpClient CLIENT = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(5))
        .build();

public static void main(String[] args) throws Exception {
    HttpRequest request = HttpRequest
            .newBuilder(URI.create("https://api.elido.app/v1/links"))
            .header("Authorization", "Bearer " + System.getenv("ELIDO_API_KEY"))
            .header("Content-Type", "application/json")
            .timeout(Duration.ofSeconds(10))
            .POST(HttpRequest.BodyPublishers.ofString(
                    "{\"destination_url\":\"https://example.com/spring-sale\"}"))
            .build();

    HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() >= 300) {
        throw new IllegalStateException(response.statusCode() + " " + response.body());
    }
    System.out.println(response.body()); // JSON with "short_url"
}

Dois timeouts fazem dois trabalhos. connectTimeout limita o handshake TCP e TLS; o timeout da requisição limita a troca inteira, e sem ele um servidor que aceita a conexão e depois silencia segura a thread. send só lança exceção em falhas de transporte e timeouts, nunca em erro HTTP, por isso a checagem de status é o contrato e não programação defensiva. Construa um único HttpClient por aplicação: ele é seguro entre threads e tem um pool de conexões, e criar um por chamada refaz o handshake TLS toda vez.

Threads virtuais atrás de um semáforo

As threads virtuais ficaram definitivas no Java 21. Elas tornam uma thread por URL barata o bastante para ser sensata. O limite de taxa, porém, não se mexeu, então o Semaphore faz o trabalho de verdade. Aqui a análise é com Jackson. Uma string JSON montada à mão quebra no primeiro destino que contiver uma aspa.

static final ObjectMapper MAPPER = new ObjectMapper();

static String shorten(String destination) throws Exception {
    String key = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
            .digest(destination.getBytes(StandardCharsets.UTF_8)));
    HttpRequest request = HttpRequest
            .newBuilder(URI.create("https://api.elido.app/v1/links"))
            .header("Authorization", "Bearer " + System.getenv("ELIDO_API_KEY"))
            .header("Content-Type", "application/json")
            .header("Idempotency-Key", key)
            .timeout(Duration.ofSeconds(10))
            .POST(HttpRequest.BodyPublishers.ofString(
                    MAPPER.writeValueAsString(Map.of("destination_url", destination))))
            .build();
    HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() >= 300) {
        throw new IllegalStateException(response.statusCode() + " " + response.body());
    }
    return MAPPER.readTree(response.body()).get("short_url").asText();
}

static Map<String, String> shortenAll(List<String> urls) {
    Map<String, String> out = new ConcurrentHashMap<>();
    Semaphore gate = new Semaphore(8); // 8 in flight, whatever urls.size() is
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        for (String url : urls) {
            executor.submit(() -> {
                gate.acquire();
                try {
                    out.put(url, shorten(url));
                } catch (Exception e) {
                    out.put(url, "ERROR: " + e.getMessage());
                } finally {
                    gate.release();
                }
                return null;
            });
        }
    } // close() waits for every task
    return out;
}

O bloco try-with-resources faz trabalho real: fechar um executor de threads virtuais espera todas as tarefas enviadas, então não há a dança com awaitTermination. No Java 11 a 17, a mesma forma funciona com sendAsync e um pool fixo. O que falha em qualquer versão é CompletableFuture.allOf sobre uma lista sem limite, que inicia tudo de uma vez e colhe um monte de 429.

Encurtar uma URL em C# e .NET

Em C#, encurtar uma URL é uma chamada a PostAsJsonAsync em um HttpClient que já carrega o cabeçalho Bearer:

using System.Net.Http.Json;
using System.Text.Json.Serialization;

var http = new HttpClient
{
    BaseAddress = new Uri("https://api.elido.app"),
    Timeout = TimeSpan.FromSeconds(10),
};
http.DefaultRequestHeaders.Authorization =
    new("Bearer", Environment.GetEnvironmentVariable("ELIDO_API_KEY"));

var response = await http.PostAsJsonAsync("/v1/links",
    new { destination_url = "https://example.com/spring-sale" });
response.EnsureSuccessStatusCode();

var link = await response.Content.ReadFromJsonAsync<LinkResponse>();
Console.WriteLine(link!.ShortUrl);

record LinkResponse([property: JsonPropertyName("short_url")] string ShortUrl);

EnsureSuccessStatusCode é a linha que importa. Sem ela, um 401 termina tranquilo, ReadFromJsonAsync devolve um record cheio de nulos e a falha aparece três camadas depois como um NullReferenceException. O objeto anônimo mantém o nome do campo em snake_case sem política de nomes; num projeto real, defina JsonNamingPolicy.SnakeCaseLower uma vez nas opções do serializador.

Um cliente tipado do IHttpClientFactory

Esse trecho serve num protótipo de console e é errado num serviço. Um HttpClient criado por chamada abre um pool de conexões novo toda vez, e descartá-lo deixa sockets em TIME_WAIT; faça isso num laço e as portas acabam. O IHttpClientFactory agrupa os handlers e os rotaciona para que mudanças de DNS sejam percebidas.

builder.Services.AddHttpClient<ElidoClient>(client =>
    {
        client.BaseAddress = new Uri("https://api.elido.app");
        client.DefaultRequestHeaders.Authorization =
            new("Bearer", builder.Configuration["Elido:ApiKey"]);
    })
    .AddStandardResilienceHandler(); // retries, timeouts, circuit breaker

public sealed class ElidoClient(HttpClient http, ILogger<ElidoClient> logger)
{
    public async Task<string> ShortenAsync(string destination, CancellationToken ct = default)
    {
        using var request = new HttpRequestMessage(HttpMethod.Post, "/v1/links")
        {
            Content = JsonContent.Create(new { destination_url = destination }),
        };
        // stable across retries, so the resilience handler can't create duplicates
        request.Headers.Add("Idempotency-Key",
            Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(destination))));

        using var response = await http.SendAsync(request, ct);
        if (!response.IsSuccessStatusCode)
        {
            var body = await response.Content.ReadAsStringAsync(ct);
            logger.LogWarning("shorten failed {Status}: {Body}", (int)response.StatusCode, body);
            throw new HttpRequestException($"shorten failed: {(int)response.StatusCode} {body}");
        }
        var link = await response.Content.ReadFromJsonAsync<LinkResponse>(ct);
        return link!.ShortUrl;
    }
}

AddStandardResilienceHandler vem de Microsoft.Extensions.Http.Resilience e traz retentativas com jitter em 429 e 5xx, timeouts e um disjuntor. Ele vai repetir um POST a menos que você mande não repetir, e o cabeçalho Idempotency-Key é o que torna isso seguro. Ler o corpo do erro antes de lançar é a outra metade: EnsureSuccessStatusCode guarda só o código de status, o que deixa você adivinhando às 3 da manhã a qual campo o 400 se opôs. Para lotes, Parallel.ForEachAsync com MaxDegreeOfParallelism = 8 limita as requisições em andamento e lê melhor que um semáforo feito à mão. Guarde a chave nos segredos de usuário localmente e em variáveis de ambiente ou num cofre em produção, nunca num appsettings.json versionado.

Encurtar uma URL em PHP, Laravel e WordPress

Em PHP, encurtar uma URL funciona com a extensão curl que vem com o PHP, então roda até em hospedagem compartilhada onde o Composer não é opção:

<?php

$ch = curl_init('https://api.elido.app/v1/links');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 10,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . getenv('ELIDO_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode(['destination_url' => 'https://example.com/spring-sale']),
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($body === false || $status >= 300) {
    throw new RuntimeException("shorten failed: HTTP {$status} {$body}");
}
echo json_decode($body, true, 512, JSON_THROW_ON_ERROR)['short_url'];

Duas linhas existem por causa de bugs que todo mundo encontra uma vez. Sem CURLOPT_RETURNTRANSFER, o curl imprime o JSON na saída e retorna true, que é a história real por trás da maioria das perguntas «minha resposta está vazia». E CURLOPT_POSTFIELDS muda para codificação de formulário multipart assim que você passa um array, então o json_encode é o que faz dela uma requisição JSON. Muitos tutoriais de PHP ainda chamam o urlshortener/v1 do Google ou o endpoint v3 do bit.ly; ambos acabaram há tempos, e o texto sobre a alternativa ao encurtador de URL do Google conta para onde foram os links goo.gl.

Três caminhos de PHP até o mesmo endpoint do encurtador: curl simples em hospedagem compartilhada, Guzzle ou Laravel em uma aplicação e wp_remote_post dentro do WordPress, todos lendo a chave de API do ambiente

O cliente Http do Laravel com retentativas

Numa aplicação Laravel ou Symfony, o cliente HTTP do Laravel (um invólucro do Guzzle) declara a política de retentativa uma vez:

use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.elido.key'))
    ->withHeaders(['Idempotency-Key' => hash('sha256', $destination)])
    ->timeout(10)
    ->retry(3, 1000, function (Throwable $e) {
        return $e instanceof ConnectionException
            || ($e instanceof RequestException
                && in_array($e->response->status(), [429, 500, 502, 503, 504], true));
    }, throw: false)
    ->post('https://api.elido.app/v1/links', ['destination_url' => $destination]);

$short = $response->successful() ? $response->json('short_url') : null;

O callback when é o que impede um 401 de ser repetido três vezes. O Guzzle puro se comporta diferente do curl: por padrão lança ClientException em 4xx e ServerException em 5xx, então capture-as ou defina http_errors como false. Para uma importação noturna, o Pool do Guzzle com 'concurrency' => 8 é a resposta do PHP ao paralelismo limitado. Uma armadilha própria do PHP: sleep() dentro de uma requisição web segura um worker do php-fpm o tempo todo, então as retentativas pertencem a scripts de CLI, jobs de fila e WP-Cron.

WordPress: wp_remote_post na publicação

O WordPress tem sua própria camada HTTP, wp_remote_post, que escolhe um transporte e funciona em hospedagens com o curl desativado. Ligue-a à publicação e guarde o resultado como meta do post:

add_action('publish_post', function (int $post_id): void {
    if (get_post_meta($post_id, '_elido_short_url', true)) {
        return; // already shortened
    }
    $response = wp_remote_post('https://api.elido.app/v1/links', [
        'timeout' => 10,
        'headers' => [
            'Authorization'   => 'Bearer ' . ELIDO_API_KEY,
            'Content-Type'    => 'application/json',
            'Idempotency-Key' => 'post-' . $post_id,
        ],
        'body' => wp_json_encode(['destination_url' => get_permalink($post_id)]),
    ]);
    if (is_wp_error($response) || wp_remote_retrieve_response_code($response) >= 300) {
        error_log('shorten failed for post ' . $post_id);
        return;
    }
    $link = json_decode(wp_remote_retrieve_body($response), true);
    update_post_meta($post_id, '_elido_short_url', $link['short_url']);
});

Defina ELIDO_API_KEY no wp-config.php, não num arquivo de plugin que vai parar num repositório público nem numa linha de opções que todo administrador consegue ler. A chave post-123 é estável. Dispare o hook duas vezes e a segunda chamada devolve o link existente. Se preferir não manter hook nenhum, o guia do encurtador de URL para WordPress compara isso com o plugin e as vias sem código.

Encurtar uma URL em Ruby

Em Ruby, encurtar uma URL é um Net::HTTP.post da biblioteca padrão, então um script não precisa de nenhuma gem:

require "json"
require "net/http"

response = Net::HTTP.post(
  URI("https://api.elido.app/v1/links"),
  { destination_url: "https://example.com/spring-sale" }.to_json,
  "Authorization" => "Bearer #{ENV.fetch('ELIDO_API_KEY')}",
  "Content-Type" => "application/json",
)
raise "shorten failed: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess)

puts JSON.parse(response.body)["short_url"]

ENV.fetch em vez de ENV[] falha na hora quando a chave falta, em vez de enviar Bearer e voltar com um 401 confuso. A checagem is_a?(Net::HTTPSuccess) é a linha que as pessoas pulam: o Net::HTTP lança exceção em sockets mortos e timeouts, e em mais nada. Net::HTTP.post também não aceita argumentos de timeout, então para qualquer coisa sem supervisão desça para Net::HTTP.start com open_timeout e read_timeout.

Faraday com middleware de retentativa

Dentro de uma aplicação Rails, o Faraday dá um único objeto de conexão com o cabeçalho de autenticação anexado, JSON nos dois sentidos e uma política de retentativa declarada. Desde o Faraday 2, o middleware de retentativa vive na gem separada faraday-retry.

# Gemfile: gem "faraday" and gem "faraday-retry"
require "digest"
require "faraday"
require "faraday/retry"

ELIDO = Faraday.new(url: "https://api.elido.app") do |f|
  f.request :json
  f.request :retry, max: 3, interval: 0.5, backoff_factor: 2,
                    methods: %i[post], # POST is not retried by default
                    retry_statuses: [429, 500, 502, 503, 504]
  f.response :json, content_type: /\bjson$/
  f.headers["Authorization"] = "Bearer #{ENV.fetch('ELIDO_API_KEY')}"
  f.options.timeout = 10
end

def shorten(destination)
  res = ELIDO.post("/v1/links", { destination_url: destination },
                   "Idempotency-Key" => Digest::SHA256.hexdigest(destination))
  raise "shorten failed: #{res.status} #{res.body}" unless res.success?

  res.body["short_url"]
end

A linha methods: %i[post] é a que a maioria dos trechos de Faraday na internet deixa de fora. De fábrica, o faraday-retry repete só verbos idempotentes (GET, PUT, DELETE, HEAD, OPTIONS), então uma configuração de POST sem ela nunca repete nada e ninguém percebe até um 503 derrubar um link. Incluir o POST é seguro aqui justamente por causa da Idempotency-Key. O middleware também respeita o Retry-After num 429. Para uma lista, uma Queue e oito threads funcionam bem até no CRuby, já que o bloqueio global é liberado durante a E/S; no Rails, envolva shorten em um Active Job para que uma retentativa dormindo não segure uma thread do Puma.

Encurtar uma URL em PowerShell

Em PowerShell, encurtar uma URL é uma chamada a Invoke-RestMethod, embutido no Windows PowerShell 5.1 e no PowerShell 7, que devolve um objeto já analisado em vez de uma string JSON:

$body = @{ destination_url = 'https://example.com/spring-sale' } | ConvertTo-Json

$link = Invoke-RestMethod -Method Post `
    -Uri 'https://api.elido.app/v1/links' `
    -Headers @{ Authorization = "Bearer $env:ELIDO_API_KEY" } `
    -ContentType 'application/json' `
    -Body $body `
    -TimeoutSec 10

$link.short_url

ConvertTo-Json não é opcional. Passe um hashtable direto para -Body e o PowerShell o codifica como formulário, e você recebe um 400 que parece problema do servidor. A surpresa para quem vem de outras linguagens vai no sentido oposto: um 401 ou 404 aqui é um erro terminante, não um valor de retorno, então você captura uma exceção em vez de conferir um código de status. O PowerShell 7 acrescenta -SkipHttpErrorCheck, que devolve a resposta em vez disso.

Uma chave em um cofre e uma coluna inteira de CSV

Uma chave digitada num arquivo .ps1 vai parar no controle de versão, no Get-History e em qualquer transcrição que o parque de máquinas tenha ligado. O módulo SecretManagement a guarda num cofre, então rotacionar uma chave é atualizar uma entrada em vez de caçar scripts em quatro servidores. A tarefa real mais comum é uma coluna de planilha, não uma URL:

$env:ELIDO_API_KEY = Get-Secret -Name ElidoApiKey -AsPlainText

function Get-ShortLink([string] $Destination) {
    $sha = [System.Security.Cryptography.SHA256]::Create()
    $key = [BitConverter]::ToString(
        $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Destination))).Replace('-', '')
    $body = @{ destination_url = $Destination } | ConvertTo-Json
    for ($attempt = 0; $attempt -lt 3; $attempt++) {
        try {
            return (Invoke-RestMethod -Method Post -Uri 'https://api.elido.app/v1/links' `
                -Headers @{ Authorization = "Bearer $env:ELIDO_API_KEY"; 'Idempotency-Key' = $key } `
                -ContentType 'application/json' -Body $body -TimeoutSec 10).short_url
        }
        catch {
            $status = [int]$_.Exception.Response.StatusCode
            if ($status -ne 429 -and $status -lt 500) { throw }  # 400, 401, 403: give up
            Start-Sleep -Seconds ([math]::Pow(2, $attempt))
        }
    }
    throw "gave up after 3 attempts: $Destination"
}

$funcDef = ${function:Get-ShortLink}.ToString()
Import-Csv .\campaign-urls.csv | ForEach-Object -ThrottleLimit 8 -Parallel {
    ${function:Get-ShortLink} = $using:funcDef   # ship the function into the runspace
    $row = $_
    $short = try { Get-ShortLink $row.destination } catch { "ERROR: $($_.Exception.Message)" }
    [pscustomobject]@{ id = $row.id; destination = $row.destination; short_url = $short }
} | Export-Csv .\campaign-urls-short.csv -NoTypeInformation

-ThrottleLimit 8 mantém oito requisições em andamento quer o arquivo tenha cinquenta linhas ou cinquenta mil. Cada iteração de -Parallel roda no próprio runspace, por isso a função entra via $using:, e toda a construção exige o PowerShell 7; no 5.1, um foreach simples é a resposta honesta para algumas centenas de linhas. Os erros caem na coluna de saída e, como a chave de idempotência vem do destino, reexecutar o arquivo depois de corrigir três linhas não cria duplicatas. Confira algumas linhas com curl.exe -sI antes de qualquer coisa ir para a impressão. O CSV já está numa planilha do Google? A importação em massa sem código faz o mesmo trabalho.

O que os SDKs fazem hoje

Existem bibliotecas cliente oficiais para quatro linguagens (TypeScript como @elido/sdk, Python, Go, Ruby) e elas seguem a especificação OpenAPI. Só o SDK de Go foi publicado até agora, como o módulo github.com/elidoapp/elido-go (v0.1.0). Os SDKs de TypeScript, Python e Ruby ainda não foram publicados no npm, PyPI nem RubyGems. É por isso que todos os exemplos acima usam o cliente HTTP próprio de cada linguagem, que também é o que damos suporte hoje em integrações de produção. Quando as outras bibliotecas saírem, a página de API e SDKs trará as instruções de instalação.

Quando saírem, espere que envolvam as rotas com escopo de workspace, então você passará explicitamente o ID do workspace e um domain_id, e que difiram no comportamento de retentativa. O cliente TypeScript acrescenta uma Idempotency-Key a todo POST, PUT ou PATCH e repete 502, 503 e 504 com backoff, deixando o 429 para você. O cliente Ruby também gera chaves. Ele lança um erro tipado de limite de taxa com retry_after, mas nunca repete sozinho. Os clientes Python e Go não repetem de jeito nenhum. Seja qual for o seu, a política de retentativa da seção de erros continua valendo.

Para um terminal em vez de uma base de código, o guia da CLI do encurtador de URL envolve a mesma chamada de criação em curl e numa função de shell, e o guia do servidor MCP conecta a API ao Claude, ao Cursor ou a qualquer outro cliente compatível com MCP.

Acesso a analytics

Os endpoints de analytics são somente leitura e ficam em /v1/workspaces/{id}/analytics/; o guia da API de analytics de links lista cada relatório, seus parâmetros e o formato da resposta. As consultas mais comuns:

  • GET .../clicks/recent?from=…&to=…: cliques individuais, os mais recentes primeiro, paginados com next_cursor. Útil para pipelines de exportação.
  • GET .../timeseries?from=…&to=…&interval=day: contagens de cliques por intervalo; interval é hour ou day, e tz define o fuso horário dos intervalos.
  • GET .../breakdown/country?from=…&to=…: detalhamento geográfico.
  • GET .../breakdown/referrer?from=…&to=…: detalhamento por referenciador.

Os outros relatórios são summary, links/top, os demais detalhamentos (host, device, browser, destination) e as listas dos primeiros (top-countries, top-regions, top-cities, top-referrers, top-destinations). from e to são datas YYYY-MM-DD e to é exclusivo; acrescente link_id para restringir qualquer relatório a um link, e limit para dimensionar detalhamentos e listas.

O fluxo bruto de cliques é o maior. Um workspace com 10M de cliques por mês produz cerca de 600 MB de JSON de eventos brutos por mês, então percorra-o em intervalos agendados em vez de numa sentada só.

Webhooks são o inverso do polling: em vez de perguntar à API o que mudou, você recebe eventos de links e domínios no seu endpoint. Configure-os em /dashboard/webhooks com uma URL de endpoint, os eventos que quiser (link.created, link.updated, link.expired e outros) e um segredo de assinatura.

Um evento click.created por clique está no roadmap e ainda não está disponível, então os dados de cliques vêm dos endpoints de analytics por enquanto. Cada entrega traz um cabeçalho X-Elido-Signature (também enviado como X-Webhook-Signature) com o valor v1=<hex>: um HMAC-SHA256, com a chave do segredo do seu endpoint, sobre o valor de X-Webhook-Timestamp, um ponto e o corpo bruto da requisição. Verifique antes de processar, ou qualquer pessoa pode postar no seu endpoint e se passar pela Elido; o guia para verificar assinaturas de webhook traz a checagem em várias linguagens.

A entrega é de pelo menos uma vez. As entregas que falham recebem três tentativas no total, com minutos de intervalo. A comparação entre webhooks e polling pesa os dois padrões de integração.

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

A integração que motiva a maioria das adoções de API se parece com isto. Sua automação de marketing cria uma campanha no Customer.io ou no HubSpot, e um hook dispara quando ela é publicada. Seu tratador cria o link curto. Depois o guarda na campanha e o devolve para o template do e-mail. Em Node:

export async function onCampaignPublished(campaign: Campaign) {
  const res = await fetch("https://api.elido.app/v1/links", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.ELIDO_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `campaign-${campaign.id}-link`,
    },
    body: JSON.stringify({
      destination_url: campaign.destinationUrl, // UTM parameters already in here
      tags: [
        "campaign",
        `campaign:${campaign.id}`,
        `batch:${campaign.batchId}`,
        campaign.channel,
      ],
    }),
    signal: AbortSignal.timeout(10_000),
  });
  if (!res.ok)
    throw new Error(`shorten failed: ${res.status} ${await res.text()}`);

  const link = await res.json();
  await campaignStore.update(campaign.id, {
    linkId: link.id,
    shortUrl: link.short_url,
  });
  return link;
}

A chave de idempotência vem do ID da campanha. Quando o hook de publicação dispara duas vezes (vai disparar, já que as entregas são de pelo menos uma vez), a segunda chamada recebe o mesmo link em vez de uma duplicata. As tags campaign: e batch: são suas próprias chaves de junção. Os relatórios de cliques se ligam à campanha por meio delas. Para a atribuição de ponta a ponta com templates UTM e encaminhamento de conversões, o guia de rastreamento UTM percorre o pipeline completo.

O que ainda não está na API

Algumas coisas que os times perguntam não estão disponíveis hoje:

  • Analytics em uma chamada. Não existe uma requisição única que devolva todos os detalhamentos de um link; hoje você pede cliques, país, referenciador, dispositivo e série temporal separadamente, então dispare-as em paralelo no seu próprio código.
  • Reenvio de webhook. O painel mostra o histórico de entregas e consegue reenviar; a API ainda não.
  • Renomear com 301. O mesmo vale para os campos utm e metadata na criação; até existirem, ponha os parâmetros UTM no destino e as chaves de junção nas tags.

Minha regra prática: se está na especificação OpenAPI, construa em cima. Se está só num post de blog, este incluído, trate como planejado.

Leituras relacionadas no blog

Este hub fica no cluster de recursos ao lado de links inteligentes explicados, que mostra como o motor de redirecionamento resolve um link na borda.

Perguntas frequentes

Como encurtar uma URL com uma API?

Envie um POST autenticado com a URL longa em um corpo JSON e leia o link curto na resposta. Na Elido, é POST https://api.elido.app/v1/links com o cabeçalho Authorization: Bearer seguido da sua chave de API e o corpo {"destination_url": "..."}; a resposta 201 traz short_url. Toda linguagem deste guia é só um invólucro dessa única requisição.

Como encurtar uma URL em Python?

Use requests.post no endpoint de links com a chave em um cabeçalho Authorization: Bearer, json={"destination_url": url} e um timeout, chame raise_for_status() e leia response.json()["short_url"]. Para milhares de URLs, mude para httpx.AsyncClient com um asyncio.Semaphore, para manter só um punhado de requisições em andamento ao mesmo tempo.

Como encurtar uma URL em JavaScript ou Node.js?

Chame fetch no Node 18 ou mais novo com o método POST, o cabeçalho Bearer e um corpo JSON, confira res.ok antes de ler o corpo e pegue short_url do JSON analisado. Rode isso em um servidor ou em uma rota serverless, nunca em código de navegador: uma chave enviada à página pode ser lida por qualquer pessoa que abrir o DevTools.

Preciso de um SDK para usar uma API de encurtador de URL?

Não. Criar um link é um POST com dois cabeçalhos, e o cliente HTTP padrão de cada linguagem consegue enviá-lo: requests ou httpx, fetch, net/http, java.net.http, HttpClient, curl, Net::HTTP e Invoke-RestMethod. Um SDK compensa quando você usa muitos endpoints e quer modelos tipados, mas as versões com HTTP simples deste guia são o que damos suporte hoje em produção.

Como impedir que as retentativas criem links curtos duplicados?

Envie um cabeçalho Idempotency-Key e mantenha-o igual em toda retentativa de uma mesma requisição lógica, por exemplo um hash do destino ou o ID do seu pedido. A Elido guarda a primeira resposta bem-sucedida por 24 horas por workspace e a reenvia para uma chave repetida, então uma retentativa depois de uma resposta perdida devolve o link original em vez de criar um segundo.

Quantos links curtos posso criar por segundo pela API?

A Elido limita o tráfego da API por workspace com um token bucket: 10 requisições por segundo com rajada de 20 no Free, 30 com rajada de 60 no Pro e 100 com rajada de 200 no Business. Uma resposta 429 traz Retry-After: 1. Para lotes grandes, POST /v1/links/bulk aceita até 100 links por requisição.

Experimente Elido

Cole uma URL, obtenha um link curto

Sem cadastro. O link vive 30 dias. Cadastre-se para mantê-lo para sempre.

Sem registro. Crie uma conta grátis e obtenha 50 links por mês.

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
how to shorten a url in python
shorten url javascript node
golang url shortener api
shorten url java c# php
shorten url ruby powershell

Continuar lendo