32 min di letturaFunzionalità

API URL shortener: accorcia un URL in Python, JS e Go

Accorcia un URL con una sola chiamata API in Python, JavaScript, Go, Java, C#, PHP, Ruby o PowerShell, poi aggiungi retry idempotenti e limiti bulk.

Marius Voß
DevRel · edge infra

Per accorciare un URL con un'API, invia una sola POST autenticata: metti l'URL lungo nel corpo JSON come destination_url, invialo a https://api.elido.app/v1/links con Authorization: Bearer <your key> e leggi short_url dalla risposta 201 Created. Questa è l'intera integrazione in ogni linguaggio. Gli otto linguaggi qui sotto differiscono solo per come scrivono la richiesta. E per il dettaglio di produzione che morde per primo.

La prima chiamata richiede cinque minuti. I guai arrivano dopo, quando il traffico di produzione colpisce e un gestore di retry crea duplicati, così la dashboard si riempie di varianti /foo-1, /foo-2 e /foo-3 della stessa destinazione e qualcuno apre un ticket. Questa guida è la panoramica sull'API di un URL shortener che vorrei avesse letto quel team: autenticazione, la prima chiamata, i quattro endpoint dei link, le chiavi di idempotenza, i codici di errore, i limiti di frequenza, e poi la stessa richiesta di link breve in otto linguaggi con l'abitudine che ogni runtime richiede.

Stai ancora scegliendo un servizio? Il confronto tra API gratuite di URL shortener affianca forme delle richieste e limiti dei piani gratuiti tra i vari fornitori. Tutto ciò che segue riguarda l'API REST di Elido.

Vai direttamente al tuo linguaggio:

  • Python: requests per un link, httpx con un semaforo per migliaia.
  • JavaScript e Node.js: fetch, perché la chiave resta fuori dal browser, un pool di worker al posto di Promise.all.
  • Go: un solo http.Client condiviso con timeout, errgroup.SetLimit per i lotti.
  • Java: l'HttpClient del JDK, thread virtuali dietro un Semaphore.
  • C# e .NET: PostAsJsonAsync, poi un client tipizzato da IHttpClientFactory.
  • PHP, Laravel e WordPress: curl, Http:: con retry, wp_remote_post alla pubblicazione.
  • Ruby: Net::HTTP.post, poi Faraday con un middleware di retry che riprova davvero una POST.
  • PowerShell: Invoke-RestMethod, una chiave custodita in un vault e un'intera colonna CSV.

Prerequisiti: una chiave API e un URL di base

Accedi alla dashboard, apri /dashboard/api-keys e crea una chiave API. Le chiavi iniziano con elido_ e vengono mostrate una sola volta. Ogni chiave è legata a un workspace, quindi una chiave emessa nel workspace A non può creare link nel workspace B, e l'API ricava il workspace dalla sola chiave. I token degli utenti macchina (per sistemi CI, strumenti interni e integrazioni machine-to-machine) stanno in /dashboard/machine-users e ruotano indipendentemente dalle chiavi personali.

Entrambi i tipi portano un ruolo predefinito del workspace (viewer, editor o admin) invece di scope per singolo endpoint, quindi un job CI che crea solo link riceve editor. La guida ai permessi delle chiavi API elenca cosa raggiunge ogni ruolo. Le modifiche ai webhook richiedono admin, e spiega perché.

La pagina delle chiavi API di Elido con tre chiavi, i loro prefissi, i ruoli editor e viewer, le date di ultimo utilizzo e il numero di richieste delle ultime 24 ore

L'URL di base è https://api.elido.app e le route dei link stanno sotto /v1. I domini di reindirizzamento (f.elido.me su Free, s.elido.me sui piani a pagamento, b.elido.me su Business) sono una superficie separata: i tuoi link brevi si risolvono lì, mentre l'API li crea e li modifica. La specifica OpenAPI 3.1 alla base del riferimento API è il contratto. Se un campo è nella specifica, è supportato.

Un'insidia coglie i nuovi account. Le scritture fatte con una chiave il cui proprietario non ha verificato l'email tornano con 403 (email_not_verified). Le letture continuano a funzionare. I token degli utenti macchina sono esenti. Verifica l'indirizzo una volta e l'errore sparisce.

Ecco la richiesta che ogni sezione per linguaggio qui sotto riproduce, in 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"}'

La risposta è 201 Created con il record del link. Ridotta ai campi che userai:

{
  "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"
}

Conserva id se vorrai mai modificare, far scadere o eliminare il link: è l'identificatore canonico, e gli slug possono cambiare mentre gli ID no. Non hai passato un domain_id, quindi l'API ha usato il dominio breve predefinito del tuo piano. Lo slug viene generato dal server a meno che tu non invii slug; gli slug generati sono base62 ([0-9A-Za-z]) e lunghi al massimo sei caratteri.

Tieni la chiave in una variabile d'ambiente (ELIDO_API_KEY in tutti gli esempi), mai nel codice sorgente, perché la chiave di uno shortener è una credenziale di spesa e chiunque la possieda può creare link sulla tua quota, il che conta soprattutto in JavaScript, come spiega la sezione su Node.

I quattro endpoint che userai davvero

L'API ha più di quattro endpoint, ma la maggior parte delle integrazioni resta dentro questo insieme. Le route piatte /v1/links ricavano il workspace dalla tua chiave; i percorsi canonici sotto /v1/workspaces/{workspace_id}/links si comportano allo stesso modo e richiedono l'ID esplicito.

Diagramma a raggiera dei quattro endpoint principali dei link attorno alla risorsa /v1/links: POST crea, GET legge, PATCH aggiorna e DELETE elimina, ciascuno con la sua insidia principale.

POST /v1/links accetta destination_url più campi facoltativi:

  • slug - un back-half a tua scelta; deve essere univoco sul dominio.
  • domain_id - per un dominio personalizzato; omettilo per ottenere il dominio breve predefinito del tuo piano.
  • title e tags - un'etichetta per la dashboard e stringhe libere su cui filtrare in seguito.
  • expires_at - un timestamp RFC 3339 dopo il quale il link smette di reindirizzare e mostra una pagina di scadenza.
  • redirect_status - 301, 302 (il predefinito) o 307.

Le password non si impostano in creazione: invia una PATCH subito dopo, e il reindirizzamento mostrerà una pagina con password prima di inoltrare. I parametri UTM vanno direttamente in destination_url, mentre le tue chiavi di join vanno in tags.

Lo slug personalizzato è il campo che morde i team in produzione. Riusa uno slug sullo stesso dominio e ottieni 409. Un gestore ingenuo che aggiunge un contatore (my-slug-1, my-slug-2) è proprio il modo in cui comincia il pasticcio di link duplicati dell'introduzione. La soluzione è una chiave di idempotenza, trattata due sezioni più avanti.

GET /v1/links/{id} restituisce il record completo, incluso short_url. I conteggi dei clic non sono nel record del link. Arrivano dagli endpoint di analytics descritti nella guida all'API di analytics dei clic.

GET /v1/links?limit=…&tags=…&host=… elenca i link con filtri (q cerca, status e folder restringono ancora). La paginazione è basata su cursore: rimanda il next_cursor della risposta come cursor e fermati quando è null.

PATCH /v1/links/{id} accetta i campi della creazione. Gli aggiornamenti comuni sono un nuovo destination_url (rotazione di campagna senza ristampare un codice QR), nuovi tag e un expires_at più lontano. Inviare un nuovo slug rinomina il link. Il vecchio slug muore subito. Non rinominare nulla che sia già stato stampato.

DELETE /v1/links/{id} restituisce 204 No Content e sposta il link nel cestino: smette di reindirizzare e sparisce dalle chiamate di elenco e lettura. Non è ancora perso. POST /v1/links/{id}/restore lo riporta indietro e GET /v1/links/trash mostra cosa c'è dentro. Dopo 30 giorni il cestino viene svuotato definitivamente.

Ogni POST, PUT e PATCH accetta un header Idempotency-Key, una stringa opaca a tua scelta. La prima volta che una chiave arriva con una risposta riuscita, il server memorizza codice di stato e corpo per 24 ore, con ambito (workspace, key). Una ripetizione della stessa chiave con la stessa richiesta restituisce la risposta memorizzata, quindi un retry restituisce il link già esistente invece di crearne un secondo.

Tre dettagli decidono se funziona per te:

  • Solo le risposte riuscite vengono memorizzate. Un 429 o un 500 non vengono messi in cache, quindi il retry successivo con la stessa chiave ottiene un secondo tentativo vero.
  • Il server calcola un'impronta di metodo, percorso e corpo. Riusa una chiave con un corpo diverso e ottieni 422 con idempotency_key_mismatch, che è un bug del chiamante, non qualcosa da riprovare.
  • DELETE non accetta una chiave; eliminare due volte è già idempotente per natura.

La chiave stessa dovrebbe derivare da qualcosa di stabile. Ricavala dall'evento di business (order-12345-link) quando ce n'è uno, oppure da un hash SHA-256 dell'URL di destinazione in un job batch. Un UUID casuale a ogni tentativo vanifica lo scopo. Uno per URL funziona finché il batch non viene rieseguito.

Pipeline in cui un hook di campagna at-least-once lancia due chiamate di creazione con la stessa chiave di idempotenza; la cache di 24 ore deduplica la seconda, quindi viene creato esattamente un link.

La finestra di 24 ore è l'insidia. Un retry al terzo giorno di un job bloccato crea un nuovo link, quindi per i batch di più giorni conserva l'id della prima creazione riuscita e cercalo prima di riemettere la richiesta. Le chiavi sono anche per workspace: la stessa chiave in due workspace crea due link, il che è corretto per un'API multi-workspace e sorprende comunque i team che le pensavano globali.

Una chiave basata sull'hash della destinazione ha un effetto collaterale da conoscere. Due campagne che puntano allo stesso URL entro 24 ore condividono un link. Se vuoi due link, metti l'ID della campagna nella chiave.

Gestione degli errori e codici di stato

Gli errori tornano come un piccolo oggetto JSON con un error leggibile e, in alcuni casi, un code leggibile dalle macchine:

{ "error": "destination_url is required" }

I codici di stato che incontrerai:

  • 400 - errore di validazione: un destination_url mancante, un expires_at malformato, un redirect_status non valido. Correggi il payload; non riprovare.
  • 401 - la chiave manca, è revocata o è scritta male. Controlla che l'header riporti Bearer con uno spazio prima della chiave.
  • 402 - la quota di link del tuo piano è esaurita. Riprovare non aiuta; passare a un piano superiore o aspettare il periodo successivo sì.
  • 403 - il ruolo della chiave non può farlo (una chiave viewer non può creare link), l'email dell'account non è verificata, oppure la destinazione è stata bloccata dalla scansione di sicurezza ("code": "destination_blocked").
  • 404 - il link non esiste o questa chiave non lo vede. Restituiamo 404 invece di 403 perché un chiamante non possa sondare risorse che non gli appartengono.
  • 409 - lo slug è già occupato su quel dominio.
  • 422 - una chiave di idempotenza riusata con un corpo diverso, o una combinazione di opzioni non consentita.
  • 429 - hai raggiunto il limite di frequenza. Attendi Retry-After e riprova con la stessa chiave di idempotenza.
  • 500, 502, 503, 504 - un guasto lato server o transitorio. Fai backoff e riprova con la stessa chiave.
Schema decisionale che divide i codici di stato dell'API in una colonna da riprovare con backoff (429, 500, 502, 503, 504) e una da non riprovare (400, 401, 403, 404, 409) per errori di programmazione e di conflitto.

Questa divisione è l'intera politica di retry, e ogni sezione per linguaggio qui sotto la implementa allo stesso modo: riprova 429 e 5xx, rinuncia su tutto il resto. Riprovare un 400 con lo stesso payload non può dare una risposta diversa, e riprovare un 401 tre volte è solo un fallimento più lento. La guida a limiti di frequenza e retry approfondisce il jitter e perché i cicli di retry alla cieca trasformano un'interruzione in due.

Limiti di frequenza e endpoint bulk

Il traffico API è misurato per workspace con un token bucket:

PianoFrequenza sostenutaBurst
Free10 req/s20
Pro30 req/s60
Business100 req/s200

Un bucket pieno ti lascia inviare il burst tutto insieme. Poi ti assesti sulla frequenza sostenuta. Ogni chiave API ha inoltre un proprio bucket oltre a quello del workspace, quindi un singolo script fuori controllo non può consumare da solo l'intera quota del workspace.

Una richiesta respinta riceve 429, un header Retry-After: 1 e un header X-RateLimit-Scope che indica quale bucket si è esaurito (workspace o api_key). Non ci sono header con la quota residua su cui regolarsi. Quindi l'approccio pratico è un tetto fisso di concorrenza più un retry su 429: otto richieste in volo sono un buon inizio su Pro, e quattro vanno bene per Free. Una stranezza mi ha colto di sorpresa. Il corpo del 429 è un testo in forma di JSON inviato come text/plain, quindi un client che analizza solo i content type JSON vedrà una stringa semplice dove si aspettava un oggetto.

Lavoro grosso? POST /v1/links/bulk accetta {"links": [...]} con fino a 100 elementi e conta come una richiesta nel limite. Risponde 200 anche quando alcuni elementi falliscono, con i conteggi ok e failed e uno status per elemento pari a ok o error, quindi leggi ogni elemento. Non saltare questo passaggio. I link restituiti portano uno slug ma non short_url, il che significa che l'URL lo costruisci tu dal tuo dominio e dallo slug. La guida all'importazione bulk da Google Sheets sposta un intero foglio di calcolo in questo modo.

Accorciare un URL nel tuo linguaggio: la forma comune

Ogni sezione da qui in poi invia la richiesta della prima chiamata e applica le stesse quattro abitudini: la chiave dall'ambiente, un timeout su ogni richiesta, il codice di stato controllato prima di analizzare il corpo e un Idempotency-Key stabile su tutto ciò che potrebbe girare due volte. Non le rispiegherò per ogni linguaggio. Cambia l'idioma. Ogni runtime ha anche un dettaglio che la gente sbaglia.

Ogni sezione si apre con la chiamata minima funzionante. Basta per uno script lanciato a mano. Poi arriva il pezzo di produzione che gli utenti di quel runtime chiedono più spesso: concorrenza in Python, Go o Java; vita del client in C#; hosting in PHP; middleware in Ruby; segreti e CSV in PowerShell. Vuoi eseguirli così come sono? Crea una chiave sul piano gratuito, esportala come ELIDO_API_KEY e ogni snippet funziona senza modifiche.

Accorciare un URL in Python

In Python, accorciare un URL è una requests.post con la chiave in un header Bearer, la destinazione in json= e un 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

La libreria requests non ha un timeout predefinito, quindi senza timeout=10 una connessione bloccata ferma lo script per sempre. raise_for_status() trasforma un 401 o un 500 in un'eccezione invece di lasciare che una chiamata fallita sembri riuscita. Se non puoi usare pip install, urllib.request della libreria standard invia la stessa POST con più cerimonie.

Bulk con httpx e un semaforo

Un ciclo di requests.post va bene per poche centinaia di link. Per decine di migliaia, il tempo se ne va ad aspettare la rete, e httpx con asyncio sovrappone queste attese. Il semaforo conta più di tutto. Limita le richieste in volo, ed è la differenza tra un batch veloce e un muro di 429 da cui il ciclo di retry deve poi uscire a fatica.

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))

I risultati sono indicizzati per URL originale. Un fallimento parziale resta visibile. Poiché la chiave è un hash della destinazione, rieseguire l'intera lista dopo aver corretto tre righe sbagliate non crea nulla due volte. Dormire dentro il semaforo è voluto: un worker a cui è stato detto di fare backoff non dovrebbe cedere il suo posto a una richiesta nuova.

Accorciare un URL in JavaScript e Node.js

In JavaScript, accorciare un URL è una POST con fetch. Node distribuisce fetch come globale dalla versione 18, quindi non c'è nulla da installare:

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);

Eseguilo con node --env-file=.env shorten.mjs. Quella riga if (!res.ok) non è decorazione: fetch rifiuta solo in caso di errore di rete o di annullamento, quindi un 401 arriva come promise risolta con ok: false. Chi usa Axios ci inciampa una volta. Di solito si manifesta come un undefined dove doveva esserci un link.

Tieni la chiave sul server

Incolla quello snippet in un componente React e si rompono due cose. CORS blocca la risposta, perché un'API autenticata con chiave non invia Access-Control-Allow-Origin per siti arbitrari. Il problema più grave sta dietro l'errore: una chiave nel codice front-end è leggibile da chiunque apra DevTools, e un prefisso NEXT_PUBLIC_ la pubblicizza. Quindi il browser invia un semplice URL alla tua route. La route aggiunge la chiave.

Una chiamata fetch del browser a un'API di URL shortener bloccata da CORS con la chiave API esposta nel bundle, a confronto con il percorso corretto in cui il browser chiama una route del server che custodisce la chiave e inoltra la richiesta

Valida l'URL che la tua route accetta, altrimenti hai costruito un open redirect con qualche passaggio in più; l'articolo sulle vulnerabilità open redirect mostra cosa ne fanno gli attaccanti.

Per un batch, resisti a Promise.all(urls.map(shorten)). Lancia ogni richiesta nello stesso tick, quindi 500 URL diventano 500 connessioni simultanee e la maggior parte torna con 429. Un piccolo pool di worker che attingono da una coda condivisa mantiene la concorrenza costante qualunque sia la lunghezza della lista:

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;
}

Sui runtime edge (Vercel Edge, Cloudflare Workers, Deno) node:crypto può mancare; calcola l'hash con crypto.subtle.digest("SHA-256", …) e il resto resta invariato.

Accorciare un URL in Go

In Go, accorciare un URL richiede net/http e encoding/json dalla libreria standard: serializza il corpo, imposta due header, decodifica short_url. Scrivilo fin dall'inizio come funzione su un client condiviso, perché è la versione che terrai:

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 non hanno alcun timeout. Nessuno. Un host che non risponde può trattenere una goroutine per minuti. Un client a livello di package mantiene vivo il pool di connessioni tra le chiamate, e NewRequestWithContext fa sì che una CLI annullata o un chiamante disconnesso fermi la richiesta invece di lasciarla in esecuzione. Se un batch apre molti più socket del previsto, alza MaxIdleConnsPerHost sul transport: il valore predefinito di 2 è invisibile in uno script e costoso quando otto goroutine condividono un host.

Fan-out limitato con errgroup.SetLimit

Una goroutine per URL costa poco a Go ed è rovinosa per il limite di frequenza: 5.000 URL diventano 5.000 richieste in un colpo. errgroup con SetLimit ti dà un fan-out con un tetto.

Una goroutine per URL che inonda l'API dello shortener di richieste soggette a limite, a confronto con un errgroup che usa SetLimit per tenere in volo un numero limitato di richieste bulk
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
}

Qui si usa volutamente un semplice errgroup.Group. errgroup.WithContext annulla tutto al primo errore, il che va bene per un passaggio di build tutto-o-niente ed è esattamente sbagliato per un importatore che deve accorciare ciò che può, andare avanti oltre le righe sbagliate e riportare i fallimenti alla fine. Da Go 1.22 la variabile del ciclo è per iterazione, quindi la vecchia riga u := u non serve più, ma il mutex non è facoltativo: go test -race segnalerà una mappa non protetta. Aggiungi un retry su 429 e 5xx dentro shorten usando select su ctx.Done() e time.After, così il backoff resta interrompibile.

Accorciare un URL in Java

In Java, accorciare un URL è una POST tramite java.net.http.HttpClient, presente nel JDK da Java 11, quindi la prima versione non richiede dipendenze 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"
}

Due timeout fanno due lavori. connectTimeout limita l'handshake TCP e TLS; il timeout della richiesta limita l'intero scambio, e senza di esso un server che accetta la connessione e poi tace trattiene il thread. send lancia eccezioni solo per errori di trasporto e timeout, mai per un errore HTTP, ed è per questo che il controllo dello stato è il contratto e non programmazione difensiva. Costruisci un solo HttpClient per applicazione: è thread-safe e possiede un pool di connessioni, e crearne uno per chiamata ripete ogni volta l'handshake TLS.

Thread virtuali dietro un semaforo

I thread virtuali sono diventati definitivi in Java 21. Rendono un thread per URL abbastanza economico da essere ragionevole. Il limite di frequenza però non si è mosso, quindi il lavoro vero lo fa il Semaphore. Qui usa Jackson per il parsing. Una stringa JSON costruita a mano si rompe alla prima destinazione che contiene un apice doppio.

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;
}

Il blocco try-with-resources fa un lavoro vero: chiudere un executor di thread virtuali attende ogni task inviato, quindi non serve alcun giro di awaitTermination. Su Java dalla 11 alla 17 la stessa struttura funziona con sendAsync e un pool fisso. Ciò che fallisce su qualsiasi versione è CompletableFuture.allOf su una lista illimitata, che avvia tutto insieme e raccoglie un mucchio di 429.

Accorciare un URL in C# e .NET

In C#, accorciare un URL è una chiamata PostAsJsonAsync su un HttpClient che porta già l'header 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 è la riga che conta. Senza di essa un 401 si completa tranquillamente, ReadFromJsonAsync restituisce un record pieno di null e il fallimento affiora tre livelli più in là come NullReferenceException. L'oggetto anonimo mantiene il nome di campo snake_case senza una naming policy; in un progetto vero, imposta una volta JsonNamingPolicy.SnakeCaseLower nelle opzioni del serializzatore.

Un client tipizzato da IHttpClientFactory

Quello snippet va bene in uno spike da console e sbagliato in un servizio. Un HttpClient creato a ogni chiamata apre ogni volta un nuovo pool di connessioni, e smaltirlo lascia socket in TIME_WAIT; fallo in un ciclo e finisci le porte. IHttpClientFactory mette in pool gli handler e li ruota in modo che i cambi DNS vengano recepiti.

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 arriva da Microsoft.Extensions.Http.Resilience e porta retry con jitter su 429 e 5xx, timeout e un circuit breaker. Riproverà una POST a meno che tu non gli dica di no, e l'header Idempotency-Key è ciò che lo rende sicuro. Leggere il corpo dell'errore prima di lanciare è l'altra metà: EnsureSuccessStatusCode conserva solo il codice di stato, e alle 3 di notte ti lascia a indovinare a quale campo ha obiettato il 400. Per il bulk, Parallel.ForEachAsync con MaxDegreeOfParallelism = 8 limita le richieste in volo e si legge meglio di un semaforo fatto a mano. Tieni la chiave negli user secrets in locale e in variabili d'ambiente o in un vault in produzione, mai in un appsettings.json committato.

Accorciare un URL in PHP, Laravel e WordPress

In PHP, accorciare un URL funziona con l'estensione curl che accompagna PHP, quindi gira anche su hosting condivisi dove Composer non è un'opzione:

<?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'];

Due righe esistono per bug in cui tutti incappano una volta. Senza CURLOPT_RETURNTRANSFER, curl stampa il JSON in output e restituisce true, la vera storia dietro la maggior parte delle domande "la mia risposta è vuota". E CURLOPT_POSTFIELDS passa alla codifica form multipart non appena gli passi un array, quindi json_encode è ciò che ne fa una richiesta JSON. Molti tutorial PHP chiamano ancora urlshortener/v1 di Google o l'endpoint v3 di bit.ly; entrambi sono spariti da tempo, e l'articolo sull'alternativa a Google URL Shortener spiega dove sono finiti i link goo.gl.

Tre percorsi PHP verso lo stesso endpoint dello shortener: curl semplice su hosting condiviso, Guzzle o Laravel in un'applicazione e wp_remote_post dentro WordPress, tutti con la chiave API letta dall'ambiente

Il client Http di Laravel con retry

In un'app Laravel o Symfony, il client HTTP di Laravel (un involucro attorno a Guzzle) dichiara la politica di retry una volta sola:

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;

Il callback when evita che un 401 venga riprovato tre volte. Guzzle puro si comporta diversamente da curl: lancia ClientException sui 4xx e ServerException sui 5xx per impostazione predefinita, quindi catturale oppure imposta http_errors su false. Per un'importazione notturna, il Pool di Guzzle con 'concurrency' => 8 è la risposta PHP al parallelismo limitato. Una trappola specifica di PHP: sleep() dentro una richiesta web trattiene per tutto il tempo un worker php-fpm, quindi i retry appartengono a script CLI, job in coda e WP-Cron.

WordPress: wp_remote_post alla pubblicazione

WordPress ha un proprio livello HTTP, wp_remote_post, che sceglie un trasporto e funziona sugli host dove curl è disabilitato. Collegalo alla pubblicazione e salva il risultato come meta del 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']);
});

Definisci ELIDO_API_KEY in wp-config.php, non in un file di plugin che finisce in un repository pubblico e non in una riga di opzioni leggibile da ogni admin. La chiave post-123 è stabile. Se l'hook scatta due volte, la seconda chiamata riceve indietro il link esistente. Se preferisci non mantenere affatto un hook, la guida allo URL shortener per WordPress confronta questo approccio con il plugin e le vie no-code.

Accorciare un URL in Ruby

In Ruby, accorciare un URL è una Net::HTTP.post della libreria standard, quindi uno script non ha bisogno di 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 al posto di ENV[] fallisce subito quando la chiave manca, invece di inviare Bearer e tornare con un confuso 401. Il controllo is_a?(Net::HTTPSuccess) è la riga che la gente salta: Net::HTTP lancia eccezioni su socket morti e timeout e su nient'altro. Inoltre Net::HTTP.post non accetta argomenti di timeout, quindi per qualsiasi cosa non presidiata passa a Net::HTTP.start con open_timeout e read_timeout.

Faraday con middleware di retry

Dentro un'app Rails, Faraday ti dà un unico oggetto di connessione con l'header di autenticazione allegato, JSON in entrambe le direzioni e una politica di retry dichiarata. Da Faraday 2 il middleware di retry vive nella gem separata 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

La riga methods: %i[post] è quella che la maggior parte degli snippet Faraday online omette. Di serie, faraday-retry riprova solo i verbi idempotenti (GET, PUT, DELETE, HEAD, OPTIONS), quindi una configurazione con POST senza di essa non riprova mai nulla e nessuno se ne accorge finché un 503 non fa perdere un link. Abilitare POST è sicuro qui proprio grazie all'Idempotency-Key. Il middleware rispetta anche Retry-After su un 429. Per una lista, una Queue e otto thread funzionano bene anche su CRuby, dato che il lock globale viene rilasciato durante l'I/O; in Rails, avvolgi shorten in un Active Job così un retry in attesa non trattiene un thread Puma.

Accorciare un URL in PowerShell

In PowerShell, accorciare un URL è una chiamata Invoke-RestMethod, integrata in Windows PowerShell 5.1 e in PowerShell 7, e restituisce un oggetto già analizzato anziché una stringa 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 non è facoltativo. Passa una hashtable direttamente a -Body e PowerShell la codifica come form, e ottieni un 400 che sembra un problema del server. La sorpresa per chi arriva da altri linguaggi va nella direzione opposta: un 401 o 404 qui è un errore terminante, non un valore di ritorno, quindi catturi un'eccezione invece di controllare un codice di stato. PowerShell 7 aggiunge -SkipHttpErrorCheck, che restituisce invece la risposta.

Una chiave in un vault e un'intera colonna CSV

Una chiave digitata in un file .ps1 finisce nel controllo di versione, in Get-History e in qualsiasi trascrizione che l'infrastruttura abbia attivato. Il modulo SecretManagement la tiene in un vault, quindi ruotare una chiave significa aggiornare una sola voce invece di cercare tra gli script su quattro server. Il compito reale più comune è una colonna di foglio di calcolo, non un singolo 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 mantiene otto richieste in volo che il file abbia cinquanta righe o cinquantamila. Ogni iterazione di -Parallel gira in un proprio runspace, ed è per questo che la funzione entra tramite $using:, e l'intero costrutto richiede PowerShell 7; su 5.1 un semplice foreach è la risposta onesta per qualche centinaio di righe. Gli errori finiscono nella colonna di output e, poiché la chiave di idempotenza deriva dalla destinazione, rieseguire il file dopo aver corretto tre righe non crea duplicati. Controlla a campione qualche riga con curl.exe -sI prima che qualsiasi cosa venga stampata. Hai già il CSV in un Google Sheet? L'importazione bulk no-code fa lo stesso lavoro.

Cosa fanno oggi gli SDK

Esistono librerie client ufficiali per quattro linguaggi (TypeScript come @elido/sdk, Python, Go, Ruby) e seguono la specifica OpenAPI. Finora è pubblicato solo l'SDK Go, come modulo github.com/elidoapp/elido-go (v0.1.0). Gli SDK TypeScript, Python e Ruby non sono ancora stati pubblicati su npm, PyPI o RubyGems. Per questo ogni esempio qui sopra usa il client HTTP del rispettivo linguaggio, che è anche ciò che oggi supportiamo per le integrazioni in produzione. Quando le altre librerie usciranno, la pagina API e SDK riporterà le istruzioni di installazione.

Una volta disponibili, aspettati che avvolgano le route con ambito workspace, quindi passerai esplicitamente l'ID del workspace e un domain_id, e che differiscano nel comportamento dei retry. Il client TypeScript aggiunge un Idempotency-Key a ogni POST, PUT o PATCH e riprova 502, 503 e 504 con backoff, lasciando 429 a te. Anche il client Ruby genera le chiavi. Solleva un errore tipizzato di limite di frequenza con retry_after ma non riprova mai da solo. I client Python e Go non riprovano affatto. Qualunque tu usi, la politica di retry della sezione sugli errori vale ancora.

Per un terminale anziché una codebase, la guida alla CLI per URL shortener avvolge la stessa chiamata di creazione in curl e in una funzione shell, e la guida al server MCP collega l'API a Claude, Cursor o a qualsiasi altro client compatibile con MCP.

Accesso agli analytics

Gli endpoint di analytics sono di sola lettura e stanno sotto /v1/workspaces/{id}/analytics/; la guida all'API di analytics dei link elenca ogni report, i suoi parametri e la forma della risposta. Le query più comuni:

  • GET .../clicks/recent?from=…&to=… - singoli clic, dal più recente, paginati con next_cursor. Utile per le pipeline di esportazione.
  • GET .../timeseries?from=…&to=…&interval=day - conteggi dei clic per intervallo; interval è hour o day, e tz imposta il fuso orario dei bucket.
  • GET .../breakdown/country?from=…&to=… - ripartizione geografica.
  • GET .../breakdown/referrer?from=…&to=… - ripartizione per referrer.

Gli altri report sono summary, links/top, le altre ripartizioni (host, device, browser, destination) e le liste dei primi (top-countries, top-regions, top-cities, top-referrers, top-destinations). from e to sono date YYYY-MM-DD e to è escluso; aggiungi link_id per restringere qualsiasi report a un solo link, e limit per dimensionare ripartizioni e liste dei primi.

Il feed grezzo dei clic è il più grande. Un workspace con 10M di clic al mese produce circa 600MB di JSON al mese di eventi grezzi, quindi scorrilo a intervalli regolari anziché in un'unica seduta.

I webhook sono l'inverso del polling: invece di chiedere all'API cosa è cambiato, ricevi sul tuo endpoint gli eventi di link e domini. Configurali in /dashboard/webhooks con l'URL dell'endpoint, gli eventi che vuoi (link.created, link.updated, link.expired e altri) e un segreto di firma.

Un evento click.created per singolo clic è nella roadmap e non ancora disponibile, quindi per ora i dati dei clic vengono dagli endpoint di analytics. Ogni consegna porta un header X-Elido-Signature (inviato anche come X-Webhook-Signature) con il valore v1=<hex>: un HMAC-SHA256, con chiave il segreto del tuo endpoint, calcolato sul valore di X-Webhook-Timestamp, un punto e il corpo grezzo della richiesta. Verificalo prima di elaborare, altrimenti chiunque può inviare al tuo endpoint e impersonare Elido; la guida alla verifica delle firme dei webhook riporta il controllo in vari linguaggi.

La consegna è at-least-once. Le consegne fallite ricevono tre tentativi in totale, a distanza di minuti. Il confronto tra webhook e polling mette a confronto i due schemi di integrazione.

Un esempio concreto: automazione di campagna

L'integrazione che motiva la maggior parte delle adozioni dell'API ha questa forma. La tua marketing automation crea una campagna in Customer.io o HubSpot, e un hook scatta alla pubblicazione. Il tuo handler crea il link breve. Poi lo salva sulla campagna e lo restituisce per il template dell'email. In 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;
}

La chiave di idempotenza deriva dall'ID della campagna. Quando l'hook di pubblicazione scatta due volte (succederà, dato che le consegne sono at-least-once), la seconda chiamata riceve lo stesso link invece di un duplicato. I tag campaign: e batch: sono le tue chiavi di join. I report dei clic si ricollegano alla campagna tramite di essi. Per un'attribuzione end-to-end con template UTM e inoltro delle conversioni, la guida al tracciamento UTM percorre l'intera pipeline.

Cosa non c'è ancora nell'API

Alcune cose che i team chiedono non sono disponibili oggi:

  • Analytics in una chiamata. Non esiste una singola richiesta che restituisca ogni ripartizione per un link; oggi richiedi separatamente clic, paese, referrer, dispositivo e serie temporale, quindi eseguile in parallelo dal tuo codice.
  • Replay dei webhook dall'API. La dashboard mostra la cronologia delle consegne e può rieseguirle; l'API non ancora.
  • Rinomina degli slug con un 301. Lo stesso vale per i campi utm e metadata in creazione; finché non esistono, metti i parametri UTM nella destinazione e le chiavi di join nei tag.

La mia regola pratica: se è nella specifica OpenAPI, costruiscici sopra. Se è solo in un articolo del blog, questo compreso, consideralo pianificato.

Correlati sul blog

Questo hub sta nel cluster delle funzionalità accanto a smart link spiegati, che racconta come il motore di reindirizzamento risolve un link all'edge.

Domande frequenti

Come si accorcia un URL con un'API?

Invia una POST autenticata con l'URL lungo in un corpo JSON e leggi il link breve dalla risposta. Su Elido è POST https://api.elido.app/v1/links con l'header Authorization: Bearer seguito dalla tua chiave API e il corpo {"destination_url": "..."}; la risposta 201 contiene short_url. Ogni linguaggio di questa guida è un involucro attorno a quell'unica richiesta.

Come si accorcia un URL in Python?

Usa requests.post sull'endpoint dei link con la chiave in un header Authorization: Bearer, json={"destination_url": url} e un timeout, chiama raise_for_status(), poi leggi response.json()["short_url"]. Per migliaia di URL passa a httpx.AsyncClient con un asyncio.Semaphore, così solo poche richieste sono in volo contemporaneamente.

Come si accorcia un URL in JavaScript o Node.js?

Chiama fetch da Node 18 o successivo con metodo POST, l'header Bearer e un corpo JSON, controlla res.ok prima di leggere il corpo e prendi short_url dal JSON analizzato. Eseguilo su un server o in una route serverless, mai nel codice del browser, perché una chiave inviata alla pagina può essere letta da chiunque apra DevTools.

Serve un SDK per usare l'API di un URL shortener?

No. Creare un link è una sola POST con due header, e il client HTTP standard di ogni linguaggio può inviarla: requests o httpx, fetch, net/http, java.net.http, HttpClient, curl, Net::HTTP e Invoke-RestMethod. Un SDK conviene quando usi molti endpoint e vuoi modelli tipizzati, ma le versioni HTTP semplici di questa guida sono ciò che oggi supportiamo in produzione.

Come evito che i retry creino link brevi duplicati?

Invia un header Idempotency-Key e mantienilo identico a ogni retry di una stessa richiesta logica, per esempio un hash della destinazione o l'ID del tuo ordine. Elido conserva la prima risposta riuscita per 24 ore per workspace e la restituisce di nuovo per una chiave ripetuta, quindi un retry dopo una risposta persa restituisce il link originale invece di crearne un secondo.

Quanti link brevi posso creare al secondo tramite l'API?

Elido limita il traffico API per workspace con un token bucket: 10 richieste al secondo con burst di 20 su Free, 30 con burst di 60 su Pro e 100 con burst di 200 su Business. Una risposta 429 include Retry-After: 1. Per i lotti grandi, POST /v1/links/bulk accetta fino a 100 link in una richiesta.

Prova Elido

Incolla un URL, ottieni un link breve

Senza registrazione. Il link vive 30 giorni. Iscriviti per conservarlo.

Senza registrazione. Crea un account gratuito per 50 link al mese.

Prova Elido

Accorciatore di URL ospitato nell'UE: domini personalizzati, analisi approfondite e API aperta. Piano gratuito - senza carta di credito.

Tag
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

Continua a leggere