Um eine URL per API zu kürzen, senden Sie einen authentifizierten POST: Legen Sie die lange URL als destination_url in einen JSON-Body, schicken Sie ihn an https://api.elido.app/v1/links mit Authorization: Bearer <Ihr Schlüssel> und lesen Sie short_url aus der Antwort 201 Created. Das ist die ganze Integration in jeder Sprache. Die acht Sprachen unten unterscheiden sich nur darin, wie sie die Anfrage schreiben. Und darin, welches Produktionsdetail zuerst zuschlägt.
Der erste Aufruf dauert fünf Minuten. Der Ärger beginnt später, wenn Produktionsverkehr eintrifft und ein Retry-Handler Duplikate erzeugt: Das Dashboard füllt sich mit /foo-1, /foo-2 und /foo-3 für ein und dasselbe Ziel, und jemand eröffnet ein Ticket. Dieser Leitfaden ist der Durchgang durch die URL-Shortener-API, den ich diesem Team vorher gewünscht hätte: Authentifizierung, der erste Aufruf, die vier Link-Endpunkte, Idempotency-Keys, Fehlercodes, Rate Limits, und dann dieselbe Kurzlink-Anfrage in acht Sprachen, jeweils mit der einen Gewohnheit, die die Laufzeit braucht.
Sie suchen noch einen Dienst? Der Vergleich kostenloser URL-Shortener-APIs stellt Anfrageformen und Free-Limits mehrerer Anbieter nebeneinander. Alles unten bezieht sich auf die REST-API von Elido.
Direkt zu Ihrer Sprache:
- Python:
requestsfür einen Link,httpxmit Semaphore für Tausende. - JavaScript und Node.js:
fetch, warum der Schlüssel nicht in den Browser gehört, ein Worker-Pool stattPromise.all. - Go: ein gemeinsamer
http.Clientmit Timeout,errgroup.SetLimitfür Bulk. - Java: der JDK-
HttpClient, Virtual Threads hinter einemSemaphore. - C# und .NET:
PostAsJsonAsync, danach ein typisierter Client ausIHttpClientFactory. - PHP, Laravel und WordPress: curl,
Http::mit Retries,wp_remote_postbeim Veröffentlichen. - Ruby:
Net::HTTP.post, danach Faraday mit einer Retry-Middleware, die einen POST tatsächlich wiederholt. - PowerShell:
Invoke-RestMethod, ein Schlüssel im Tresor und eine ganze CSV-Spalte.
Voraussetzungen: API-Schlüssel und Basis-URL
Melden Sie sich im Dashboard an, öffnen Sie /dashboard/api-keys und erstellen Sie einen API-Schlüssel. Schlüssel beginnen mit elido_ und werden nur einmal angezeigt. Jeder Schlüssel ist an einen Workspace gebunden: Ein in Workspace A ausgestellter Schlüssel kann in Workspace B keine Links erstellen, und die API erkennt allein am Schlüssel, welchen Workspace Sie meinen. Tokens für Maschinenbenutzer (für CI-Systeme, interne Werkzeuge und Machine-to-Machine-Integrationen) liegen unter /dashboard/machine-users und werden unabhängig von persönlichen Schlüsseln rotiert.
Beide Arten tragen eine voreingestellte Workspace-Rolle (viewer, editor oder admin) statt Rechten pro Endpunkt, also bekommt ein CI-Job, der nur Links erstellt, editor. Der Leitfaden zu API-Schlüssel-Berechtigungen listet auf, was jede Rolle erreicht. Webhook-Änderungen brauchen admin, und dort steht auch, warum.
Die Basis-URL ist https://api.elido.app, und die Link-Routen liegen unter /v1. Die Weiterleitungsdomains (f.elido.me bei Free, s.elido.me in bezahlten Tarifen, b.elido.me bei Business) sind eine eigene Oberfläche: Ihre Kurzlinks werden dort aufgelöst, während die API sie anlegt und bearbeitet. Die OpenAPI-3.1-Spezifikation hinter der API-Referenz ist der Vertrag. Steht ein Feld in der Spezifikation, wird es unterstützt.
Eine Falle erwischt neue Konten. Schreibzugriffe mit einem Schlüssel, dessen Besitzer die E-Mail-Adresse nicht bestätigt hat, kommen als 403 (email_not_verified) zurück. Lesen funktioniert weiter. Tokens für Maschinenbenutzer sind ausgenommen. Bestätigen Sie die Adresse einmal, und der Fehler verschwindet.
Der erste Aufruf: einen Kurzlink erstellen
Hier ist die Anfrage, die jeder Sprachabschnitt unten nachbaut, 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"}'
Die Antwort ist 201 Created mit dem Link-Datensatz. Gekürzt auf die Felder, die Sie brauchen:
{
"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"
}
Speichern Sie id, falls Sie den Link später bearbeiten, ablaufen lassen oder löschen wollen: Sie ist der kanonische Bezeichner, und Slugs können sich ändern, IDs nicht. Sie haben keine domain_id übergeben, also hat die API die Standard-Kurzdomain Ihres Tarifs genommen. Den Slug erzeugt der Server, sofern Sie keinen slug senden; erzeugte Slugs bestehen aus base62 ([0-9A-Za-z]) und sind höchstens sechs Zeichen lang.
Halten Sie den Schlüssel in einer Umgebungsvariable (ELIDO_API_KEY in allen Beispielen hier), nie im Quellcode, denn ein Shortener-Schlüssel ist eine Zahlungsberechtigung: Wer ihn hat, kann Links auf Ihre Quote erstellen. Am meisten zählt das in JavaScript, wie der Node-Abschnitt erklärt.
Die vier Endpunkte, die Sie tatsächlich nutzen
Die API hat mehr als vier Endpunkte, aber die meisten Integrationen bleiben in diesem Satz. Die flachen Routen unter /v1/links leiten den Workspace aus Ihrem Schlüssel ab; die kanonischen Pfade unter /v1/workspaces/{workspace_id}/links verhalten sich gleich und verlangen die ausgeschriebene ID.
Einen Link erstellen
POST /v1/links nimmt destination_url plus optionale Felder:
slug- ein Back-Half Ihrer Wahl; er muss auf der Domain eindeutig sein.domain_id- für eine eigene Domain; lassen Sie sie weg, um die Standard-Kurzdomain Ihres Tarifs zu bekommen.titleundtags- ein Dashboard-Label und freie Strings, nach denen Sie später filtern können.expires_at- ein RFC-3339-Zeitstempel, nach dem der Link nicht mehr weiterleitet und eine Seite für abgelaufene Links zeigt.redirect_status-301,302(der Standard) oder307.
Passwörter werden beim Erstellen nicht gesetzt: Senden Sie direkt danach einen PATCH, und die Weiterleitung zeigt vor dem Durchleiten eine Passwortseite. UTM-Parameter gehören direkt in destination_url, und Ihre eigenen Join-Schlüssel in tags.
Der eigene Slug ist das Feld, das Teams in der Produktion beißt. Wer einen Slug auf derselben Domain wiederverwendet, bekommt 409. Ein naiver Handler, der einen Zähler anhängt (my-slug-1, my-slug-2), löst genau das Duplikat-Chaos vom Anfang aus. Die Lösung ist ein Idempotency-Key, zwei Abschnitte weiter unten.
Links lesen und auflisten
GET /v1/links/{id} liefert den vollständigen Datensatz, einschließlich short_url. Klickzahlen stehen nicht im Link-Datensatz. Sie kommen aus den Analytics-Endpunkten, die im Leitfaden zur Klick-Analytics-API beschrieben sind.
GET /v1/links?limit=…&tags=…&host=… listet Links mit Filtern auf (q sucht, status und folder grenzen weiter ein). Die Paginierung ist cursorbasiert: Senden Sie next_cursor aus der Antwort als cursor zurück und hören Sie auf, wenn er null ist.
Einen Link aktualisieren
PATCH /v1/links/{id} akzeptiert die Felder des Erstellens. Typische Änderungen sind ein neues destination_url (Kampagnenrotation, ohne einen QR-Code neu zu drucken), neue Tags und ein späteres expires_at. Ein neuer slug benennt den Link um. Der alte Slug ist sofort tot. Benennen Sie nichts um, was schon gedruckt ist.
Einen Link löschen und wiederherstellen
DELETE /v1/links/{id} liefert 204 No Content und verschiebt den Link in den Papierkorb: Er leitet nicht mehr weiter und verschwindet aus Listen- und Lese-Aufrufen. Er ist aber noch nicht endgültig weg. POST /v1/links/{id}/restore holt ihn zurück, und GET /v1/links/trash zeigt, was im Papierkorb liegt. Nach 30 Tagen wird der Papierkorb endgültig geleert.
Idempotency-Keys: Retries ohne doppelte Links
Jeder POST, PUT und PATCH akzeptiert einen Idempotency-Key-Header, einen undurchsichtigen String Ihrer Wahl. Trifft ein Schlüssel zum ersten Mal mit einer erfolgreichen Antwort ein, speichert der Server Statuscode und Body 24 Stunden lang, bezogen auf (Workspace, Schlüssel). Wiederholen Sie denselben Schlüssel mit derselben Anfrage, bekommen Sie die gespeicherte Antwort zurück, sodass ein Retry den bereits vorhandenen Link liefert, statt einen zweiten zu erzeugen.
Drei Details entscheiden, ob das bei Ihnen funktioniert:
- Nur erfolgreiche Antworten werden gespeichert. Ein
429oder500wird nicht gecacht, sodass der nächste Retry mit demselben Schlüssel einen echten zweiten Versuch bekommt. - Der Server bildet einen Fingerabdruck aus Methode, Pfad und Body. Verwenden Sie einen Schlüssel mit anderem Body erneut, bekommen Sie
422mitidempotency_key_mismatch, ein Fehler des Aufrufers und nichts, was man wiederholt. DELETEnimmt keinen Schlüssel an; zweimal zu löschen ist von Natur aus idempotent.
Der Schlüssel selbst sollte aus etwas Stabilem stammen. Leiten Sie ihn aus dem Geschäftsereignis ab (order-12345-link), wenn es eines gibt, oder aus einem SHA-256-Hash der Ziel-URL in einem Batch-Job. Eine zufällige UUID pro Versuch verfehlt den Zweck. Einer pro URL funktioniert, bis der Batch erneut läuft.
Der Haken ist das 24-Stunden-Fenster. Ein Retry am dritten Tag eines hängenden Jobs erzeugt einen neuen Link, also speichern Sie bei mehrtägigen Batches die id aus dem ersten erfolgreichen Erstellen und schlagen Sie sie nach, bevor Sie erneut senden. Schlüssel gelten außerdem pro Workspace: Derselbe Schlüssel in zwei Workspaces erzeugt zwei Links, was für eine Multi-Workspace-API richtig ist und Teams trotzdem überrascht, die Schlüssel für global hielten.
Ein Hash des Ziels als Schlüssel hat einen Nebeneffekt, den Sie kennen sollten. Zwei Kampagnen mit derselben URL teilen sich innerhalb von 24 Stunden einen Link. Wollen Sie zwei Links, nehmen Sie die Kampagnen-ID in den Schlüssel auf.
Fehlerbehandlung und Statuscodes
Fehler kommen als kleines JSON-Objekt mit einem menschenlesbaren error und in manchen Fällen einem maschinenlesbaren code:
{ "error": "destination_url is required" }
Die Statuscodes, die Ihnen begegnen:
400- Validierungsfehler: ein fehlendesdestination_url, ein fehlerhaftesexpires_at, ein ungültigesredirect_status. Korrigieren Sie den Body; wiederholen Sie nicht.401- der Schlüssel fehlt, wurde widerrufen oder falsch getippt. Prüfen Sie, dass der HeaderBearermit einem Leerzeichen vor dem Schlüssel enthält.402- das Link-Kontingent Ihres Tarifs ist aufgebraucht. Ein Retry hilft nicht; ein Upgrade oder das Warten auf den nächsten Zeitraum schon.403- die Rolle des Schlüssels erlaubt das nicht (einviewer-Schlüssel kann keine Links erstellen), die E-Mail-Adresse des Kontos ist nicht bestätigt, oder das Ziel wurde von der Sicherheitsprüfung blockiert ("code": "destination_blocked").404- der Link existiert nicht, oder dieser Schlüssel sieht ihn nicht. Wir liefern 404 statt 403, damit ein Aufrufer nicht nach fremden Ressourcen suchen kann.409- der Slug ist auf dieser Domain vergeben.422- ein Idempotency-Key wurde mit anderem Body wiederverwendet, oder eine nicht erlaubte Optionskombination.429- Sie haben das Rate Limit erreicht. Warten Sie aufRetry-Afterund versuchen Sie es mit demselben Idempotency-Key erneut.500,502,503,504- ein serverseitiger oder vorübergehender Fehler. Warten Sie kurz und wiederholen Sie mit demselben Schlüssel.
Diese Trennung ist die gesamte Retry-Richtlinie, und jeder Sprachabschnitt unten setzt sie gleich um: 429 und 5xx wiederholen, bei allem anderen aufgeben. Einen 400 mit demselben Body zu wiederholen kann keine andere Antwort bringen, und einen 401 dreimal zu wiederholen ist nur ein langsamerer Fehlschlag. Der Leitfaden zu Rate Limits und Retries geht tiefer auf Jitter ein und darauf, warum blinde Retry-Schleifen aus einem Ausfall zwei machen.
Rate Limits und der Bulk-Endpunkt
API-Verkehr wird pro Workspace mit einem Token Bucket gemessen:
| Tarif | Dauerrate | Burst |
|---|---|---|
| Free | 10 Anfr./s | 20 |
| Pro | 30 Anfr./s | 60 |
| Business | 100 Anfr./s | 200 |
Ein voller Bucket lässt Sie den Burst auf einmal senden. Danach pendeln Sie sich auf der Dauerrate ein. Zusätzlich zum Workspace-Bucket hat jeder API-Schlüssel einen eigenen, sodass ein einzelnes außer Kontrolle geratenes Skript nicht das gesamte Kontingent des Workspace allein verbraucht.
Eine abgelehnte Anfrage bekommt 429, einen Header Retry-After: 1 und einen Header X-RateLimit-Scope, der sagt, welcher Bucket leer war (workspace oder api_key). Header mit verbleibendem Kontingent, nach denen Sie sich richten könnten, gibt es nicht. Praktisch heißt das: eine feste Obergrenze für die Parallelität plus ein Retry bei 429. Acht Anfragen gleichzeitig sind bei Pro ein bequemer Start, vier passen zu Free. Eine Eigenheit hat mich erwischt. Der 429-Body ist JSON-förmiger Text, der als text/plain gesendet wird, sodass ein Client, der nur JSON-Content-Types parst, einen einfachen String sieht, wo er ein Objekt erwartet hat.
Große Menge? POST /v1/links/bulk nimmt {"links": [...]} mit bis zu 100 Einträgen und zählt als eine Anfrage gegen das Limit. Er antwortet mit 200, auch wenn einzelne Einträge scheitern, mit Zählern ok und failed und einem status von ok oder error pro Eintrag, also lesen Sie jeden einzelnen. Überspringen Sie das nicht. Die zurückgegebenen Links tragen einen slug, aber keine short_url, Sie bauen die URL also selbst aus Ihrer Domain und dem Slug. Die Anleitung zum Bulk-Import aus Google Sheets bewegt so eine ganze Tabelle.
Eine URL in Ihrer Sprache kürzen: das gemeinsame Muster
Jeder Abschnitt ab hier sendet die Anfrage aus dem ersten Aufruf und wendet dieselben vier Gewohnheiten an: der Schlüssel aus der Umgebung, ein Timeout bei jeder Anfrage, der Statuscode wird vor dem Parsen des Bodys geprüft, und ein stabiler Idempotency-Key bei allem, was zweimal laufen könnte. Ich erkläre sie nicht in jeder Sprache neu. Was sich ändert, ist die Redeweise. Jede Laufzeit hat zudem ein Detail, das Leute falsch machen.
Jeder Abschnitt beginnt mit dem minimalen funktionierenden Aufruf. Das reicht für ein Skript, das Sie von Hand starten. Danach folgt das Produktionsstück, nach dem die Nutzer dieser Laufzeit am häufigsten fragen: Parallelität in Python, Go oder Java; Lebensdauer des Clients in C#; Hosting in PHP; Middleware in Ruby; Geheimnisse und CSV in PowerShell. Sie wollen sie unverändert ausführen? Erstellen Sie einen Schlüssel im Free-Tarif, exportieren Sie ihn als ELIDO_API_KEY, und jedes Snippet läuft unverändert.
Eine URL in Python kürzen
In Python ist das Kürzen einer URL ein einzelnes requests.post mit dem Schlüssel im Bearer-Header, dem Ziel in json= und einem 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
Die requests-Bibliothek hat kein Standard-Timeout, ohne timeout=10 blockiert eine hängende Verbindung das Skript also für immer. raise_for_status() macht aus einem 401 oder 500 eine Exception, damit ein fehlgeschlagener Aufruf nicht wie ein Erfolg aussieht. Wenn Sie nichts per pip install installieren können, sendet urllib.request aus der Standardbibliothek denselben POST mit mehr Zeremonie.
Bulk mit httpx und einer Semaphore
Eine Schleife über requests.post genügt für ein paar hundert Links. Bei Zehntausenden geht die Zeit mit dem Warten aufs Netz drauf, und httpx mit asyncio lässt diese Wartezeiten überlappen. Die Semaphore ist dabei am wichtigsten. Sie begrenzt die Anfragen im Flug, und das ist der Unterschied zwischen einem schnellen Batch und einer Wand aus 429, aus der die Retry-Schleife sich dann wieder herausgraben muss.
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))
Die Ergebnisse sind nach der ursprünglichen URL indiziert. Ein Teilausfall bleibt sichtbar. Weil der Schlüssel ein Hash des Ziels ist, erzeugt ein erneuter Lauf der gesamten Liste nach der Korrektur von drei fehlerhaften Zeilen nichts doppelt. Das Schlafen innerhalb der Semaphore ist Absicht: Ein Worker, dem gesagt wurde, zurückzutreten, soll seinen Platz nicht an eine frische Anfrage abgeben.
Eine URL in JavaScript und Node.js kürzen
In JavaScript ist das Kürzen einer URL ein einzelner fetch-POST. Node liefert fetch seit Version 18 als globale Funktion, Sie müssen also nichts installieren:
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);
Starten Sie es mit node --env-file=.env shorten.mjs. Die Zeile if (!res.ok) ist keine Zierde: fetch lehnt nur bei einem Netzwerkfehler oder einem Abbruch ab, ein 401 kommt also als aufgelöstes Promise mit ok: false an. Axios-Nutzer erwischt das einmal. Meist zeigt es sich als undefined, wo ein Link sein sollte.
Den Schlüssel auf dem Server halten
Fügen Sie dieses Snippet in eine React-Komponente ein, und zwei Dinge gehen kaputt. CORS blockiert die Antwort, weil eine mit Schlüssel authentifizierte API für beliebige Websites kein Access-Control-Allow-Origin sendet. Das größere Problem steckt hinter dem Fehler: Ein Schlüssel im Frontend-Code ist für jeden lesbar, der die DevTools öffnet, und ein NEXT_PUBLIC_-Präfix hängt ihn öffentlich aus. Also schickt der Browser eine einfache URL an Ihre Route. Die Route fügt den Schlüssel hinzu.
Validieren Sie die URL, die Ihre Route annimmt, sonst haben Sie einen offenen Redirect mit zusätzlichen Schritten gebaut; der Beitrag zu Open-Redirect-Schwachstellen zeigt, was Angreifer damit anfangen.
Für einen Batch sollten Sie nicht zu Promise.all(urls.map(shorten)) greifen. Es feuert alle Anfragen im selben Tick ab, aus 500 URLs werden also 500 gleichzeitige Verbindungen, und die meisten kommen mit 429 zurück. Ein kleiner Pool von Workern, die aus einer gemeinsamen Queue ziehen, hält die Parallelität flach, egal wie lang die Liste wird:
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;
}
In Edge-Laufzeiten (Vercel Edge, Cloudflare Workers, Deno) kann node:crypto fehlen; hashen Sie dann mit crypto.subtle.digest("SHA-256", …), der Rest bleibt gleich.
Eine URL in Go kürzen
In Go braucht das Kürzen einer URL net/http und encoding/json aus der Standardbibliothek: den Body marshallen, zwei Header setzen, short_url dekodieren. Schreiben Sie es von Anfang an als Funktion auf einem gemeinsamen Client, denn diese Version behalten Sie am Ende:
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 und http.Post haben überhaupt kein Timeout. Keines. Ein nicht antwortender Host kann eine Goroutine minutenlang festhalten. Ein Client auf Paketebene hält den Verbindungspool über Aufrufe hinweg am Leben, und NewRequestWithContext sorgt dafür, dass ein abgebrochenes CLI oder ein getrennter Aufrufer die Anfrage stoppt, statt sie weiterlaufen zu lassen. Öffnet ein Batch deutlich mehr Sockets als erwartet, erhöhe MaxIdleConnsPerHost am Transport: Der Standardwert 2 fällt in einem Skript nicht auf und wird teuer, sobald acht Goroutinen sich einen Host teilen.
Begrenztes Fan-out mit errgroup.SetLimit
Eine Goroutine pro URL ist für Go billig und für das Rate Limit ruinös: 5.000 URLs werden zu 5.000 Anfragen auf einmal. errgroup mit SetLimit gibt Ihnen ein Fan-out mit Obergrenze.
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
}
Das hier ist absichtlich eine einfache errgroup.Group. errgroup.WithContext bricht beim ersten Fehler alles ab, was zu einem Alles-oder-nichts-Build-Schritt passt und für einen Importer genau falsch ist, der kürzen soll, was er kann, an den fehlerhaften Zeilen vorbeigeht und die Fehlschläge am Ende meldet. Seit Go 1.22 gilt die Schleifenvariable pro Iteration, die alte Zeile u := u entfällt also, aber der Mutex ist nicht optional: go test -race markiert eine ungeschützte Map. Ergänzen Sie in shorten einen Retry bei 429 und 5xx mit select auf ctx.Done() und time.After, damit das Warten unterbrechbar bleibt.
Eine URL in Java kürzen
In Java ist das Kürzen einer URL ein POST über java.net.http.HttpClient, der seit Java 11 im JDK steckt, die erste Version braucht also keine Maven-Abhängigkeit:
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"
}
Zwei Timeouts erledigen zwei Aufgaben. connectTimeout begrenzt den TCP- und TLS-Handshake; das timeout der Anfrage begrenzt den gesamten Austausch, und ohne es hält ein Server, der die Verbindung annimmt und dann verstummt, den Thread fest. send wirft nur bei Transportfehlern und Timeouts, nie bei einem HTTP-Fehler, weshalb die Statusprüfung hier der Vertrag ist und keine übertriebene Vorsicht. Bauen Sie einen HttpClient pro Anwendung: Er ist threadsicher und besitzt einen Verbindungspool, und pro Aufruf einen neuen zu erstellen wiederholt jedes Mal den TLS-Handshake.
Virtual Threads hinter einer Semaphore
Virtual Threads wurden in Java 21 final. Sie machen einen Thread pro URL billig genug, um vernünftig zu sein. Das Rate Limit hat sich aber nicht bewegt, also leistet der Semaphore die eigentliche Arbeit. Parsen Sie hier mit Jackson. Ein von Hand gebauter JSON-String bricht beim ersten Ziel mit Anführungszeichen.
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;
}
Der Try-with-resources-Block leistet echte Arbeit: Das Schließen eines Virtual-Thread-Executors wartet auf jede übergebene Aufgabe, ein awaitTermination-Tanz entfällt also. Auf Java 11 bis 17 funktioniert dieselbe Form mit sendAsync und einem festen Pool. Auf keiner Version funktioniert CompletableFuture.allOf über eine unbegrenzte Liste: Es startet alles auf einmal und sammelt einen Haufen 429.
Eine URL in C# und .NET kürzen
In C# ist das Kürzen einer URL ein einzelner PostAsJsonAsync-Aufruf auf einem HttpClient, der den Bearer-Header schon trägt:
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 ist die Zeile, auf die es ankommt. Ohne sie läuft ein 401 fröhlich durch, ReadFromJsonAsync liefert einen Record voller Nullwerte, und der Fehler taucht drei Schichten weiter als NullReferenceException auf. Das anonyme Objekt behält den snake_case-Feldnamen ohne Namensrichtlinie; setzen Sie in einem echten Projekt einmal JsonNamingPolicy.SnakeCaseLower in den Serializer-Optionen.
Ein typisierter Client aus IHttpClientFactory
Dieses Snippet ist in einem Konsolen-Spike in Ordnung und in einem Dienst falsch. Ein pro Aufruf erzeugter HttpClient öffnet jedes Mal einen neuen Verbindungspool, und sein Freigeben lässt Sockets in TIME_WAIT zurück; in einer Schleife gehen Ihnen die Ports aus. IHttpClientFactory bündelt die Handler und rotiert sie, damit DNS-Änderungen ankommen.
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 kommt aus Microsoft.Extensions.Http.Resilience und bringt Retries mit Jitter bei 429 und 5xx, Timeouts und einen Circuit Breaker mit. Er wiederholt einen POST, sofern Sie es ihm nicht verbieten, und der Idempotency-Key-Header macht das sicher. Die andere Hälfte ist, den Fehler-Body vor dem Werfen zu lesen: EnsureSuccessStatusCode behält nur den Statuscode, und um 3 Uhr nachts raten Sie dann, an welchem Feld sich der 400 gestört hat. Für Bulk begrenzt Parallel.ForEachAsync mit MaxDegreeOfParallelism = 8 die Anfragen im Flug und liest sich besser als eine selbstgebaute Semaphore. Halten Sie den Schlüssel lokal in den User Secrets und in der Produktion in Umgebungsvariablen oder einem Tresor, nie in einer eingecheckten appsettings.json.
Eine URL in PHP, Laravel und WordPress kürzen
In PHP funktioniert das Kürzen einer URL mit der curl-Erweiterung, die mit PHP ausgeliefert wird, sie läuft also selbst auf Shared Hosting, wo Composer keine Option ist:
<?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'];
Zwei Zeilen gibt es wegen Fehlern, in die jeder einmal läuft. Ohne CURLOPT_RETURNTRANSFER gibt curl das JSON direkt aus und liefert true zurück, was die eigentliche Geschichte hinter den meisten »meine Antwort ist leer«-Fragen ist. Und CURLOPT_POSTFIELDS schaltet auf Multipart-Formkodierung um, sobald Sie ein Array übergeben, also macht erst das json_encode daraus eine JSON-Anfrage. Viele PHP-Tutorials rufen noch urlshortener/v1 von Google oder den bit.ly-v3-Endpunkt auf; beide sind lange weg, und der Beitrag zur Google-URL-Shortener-Alternative erklärt, wohin goo.gl-Links verschwunden sind.
Laravels Http-Client mit Retries
In einer Laravel- oder Symfony-App legt der Laravel-HTTP-Client (ein Wrapper um Guzzle) die Retry-Richtlinie einmal fest:
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;
Der when-Callback verhindert, dass ein 401 dreimal wiederholt wird. Reines Guzzle verhält sich anders als curl: Es wirft standardmäßig ClientException bei 4xx und ServerException bei 5xx, fangen Sie sie also ab oder setzen Sie http_errors auf false. Für einen nächtlichen Import ist Guzzles Pool mit 'concurrency' => 8 die PHP-Antwort auf begrenzte Parallelität. Eine PHP-typische Falle: sleep() in einer Web-Anfrage hält die ganze Zeit einen php-fpm-Worker fest, Retries gehören also in CLI-Skripte, Queue-Jobs und WP-Cron.
WordPress: wp_remote_post beim Veröffentlichen
WordPress hat eine eigene HTTP-Schicht, wp_remote_post, die einen Transport auswählt und auch auf Hosts funktioniert, auf denen curl deaktiviert ist. Hängen Sie sie ans Veröffentlichen und speichern Sie das Ergebnis als Post-Meta:
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']);
});
Definieren Sie ELIDO_API_KEY in wp-config.php, nicht in einer Plugin-Datei, die in einem öffentlichen Repository landet, und nicht in einer Options-Zeile, die jeder Admin lesen kann. Der Schlüssel post-123 ist stabil. Feuert der Hook zweimal, liefert der zweite Aufruf den vorhandenen Link zurück. Wollen Sie gar keinen Hook pflegen, vergleicht der Leitfaden zum URL-Shortener für WordPress diesen Weg mit dem Plugin und den No-Code-Wegen.
Eine URL in Ruby kürzen
In Ruby ist das Kürzen einer URL ein Net::HTTP.post aus der Standardbibliothek, ein Skript braucht also kein 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 statt ENV[] schlägt sofort fehl, wenn der Schlüssel fehlt, statt Bearer zu senden und mit einem verwirrenden 401 zurückzukommen. Die Prüfung is_a?(Net::HTTPSuccess) ist die Zeile, die Leute überspringen: Net::HTTP wirft bei toten Sockets und Timeouts und bei sonst nichts. Net::HTTP.post nimmt außerdem keine Timeout-Argumente, für alles Unbeaufsichtigte wechseln Sie also zu Net::HTTP.start mit open_timeout und read_timeout.
Faraday mit Retry-Middleware
In einer Rails-App gibt Ihnen Faraday ein Verbindungsobjekt mit angehängtem Auth-Header, JSON in beide Richtungen und einer deklarierten Retry-Richtlinie. Seit Faraday 2 steckt die Retry-Middleware im separaten Gem 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
Die Zeile methods: %i[post] lassen die meisten Faraday-Snippets im Netz weg. Ab Werk wiederholt faraday-retry nur idempotente Verben (GET, PUT, DELETE, HEAD, OPTIONS), eine POST-Konfiguration ohne sie wiederholt also nichts, und niemand merkt es, bis ein 503 einen Link verliert. POST freizugeben ist hier sicher, gerade wegen des Idempotency-Key. Die Middleware beachtet auch Retry-After bei einem 429. Für eine Liste funktionieren eine Queue und acht Threads selbst in CRuby gut, weil die globale Sperre während des IO freigegeben wird; in Rails verpacken Sie shorten in einen Active Job, damit ein schlafender Retry keinen Puma-Thread festhält.
Eine URL in PowerShell kürzen
In PowerShell ist das Kürzen einer URL ein einzelner Invoke-RestMethod-Aufruf, eingebaut in Windows PowerShell 5.1 und PowerShell 7, und er liefert ein geparstes Objekt statt eines JSON-Strings:
$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 ist nicht optional. Reichen Sie eine Hashtable direkt an -Body weiter, kodiert PowerShell sie als Formular, und Sie bekommen einen 400, der wie ein Serverproblem aussieht. Die Überraschung für Leute aus anderen Sprachen läuft in die andere Richtung: Ein 401 oder 404 ist hier ein abbrechender Fehler und kein Rückgabewert, Sie fangen also eine Exception ab, statt einen Statuscode zu prüfen. PowerShell 7 ergänzt -SkipHttpErrorCheck, das stattdessen die Antwort zurückgibt.
Ein Schlüssel im Tresor und eine ganze CSV-Spalte
Ein Schlüssel, der in eine .ps1-Datei getippt wird, landet in der Versionsverwaltung, in Get-History und in jedem Transkript-Logging, das die Umgebung eingeschaltet hat. Das SecretManagement-Modul hält ihn in einem Tresor, sodass eine Schlüsselrotation bedeutet, einen Eintrag zu aktualisieren, statt Skripte auf vier Servern abzusuchen. Die häufige echte Aufgabe ist eine Tabellenspalte statt einer einzelnen 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 hält acht Anfragen im Flug, egal ob die Datei fünfzig oder fünfzigtausend Zeilen hat. Jede -Parallel-Iteration läuft in einem eigenen Runspace, weshalb die Funktion über $using: hineinwandert, und das ganze Konstrukt braucht PowerShell 7; unter 5.1 ist ein einfaches foreach für ein paar hundert Zeilen die ehrliche Antwort. Fehler landen in der Ausgabespalte, und weil der Idempotency-Key aus dem Ziel stammt, erzeugt ein erneuter Lauf der Datei nach der Korrektur von drei Zeilen keine Duplikate. Prüfen Sie ein paar Zeilen stichprobenartig mit curl.exe -sI, bevor etwas gedruckt wird. Die CSV liegt schon in einem Google Sheet? Der No-Code-Bulk-Import erledigt dieselbe Arbeit.
Was die SDKs heute können
Offizielle Client-Bibliotheken gibt es für vier Sprachen (TypeScript als @elido/sdk, Python, Go, Ruby), und sie folgen der OpenAPI-Spezifikation. Bisher ist nur das Go-SDK als Modul github.com/elidoapp/elido-go (v0.1.0) veröffentlicht. Die SDKs für TypeScript, Python und Ruby sind noch nicht auf npm, PyPI oder RubyGems veröffentlicht. Deshalb nutzt jedes Beispiel oben den eigenen HTTP-Client der jeweiligen Sprache, den wir heute auch für Produktivintegrationen unterstützen. Sobald die Bibliotheken erscheinen, trägt die Seite zu API und SDKs Installationsanweisungen.
Erwarten Sie, dass sie dann die Workspace-bezogenen Routen umhüllen, Sie Workspace-ID und domain_id also ausdrücklich übergeben, und dass sie sich im Retry-Verhalten unterscheiden. Der TypeScript-Client fügt jedem POST, PUT oder PATCH einen Idempotency-Key hinzu und wiederholt 502, 503 und 504 mit Backoff, 429 überlässt er Ihnen. Der Ruby-Client erzeugt ebenfalls Schlüssel. Er wirft einen typisierten Rate-Limit-Fehler mit retry_after, wiederholt aber nie von selbst. Die Python- und Go-Clients wiederholen gar nicht. Welchen Sie auch nutzt, die Retry-Richtlinie aus dem Fehlerabschnitt gilt weiter.
Für ein Terminal statt einer Codebasis verpackt der Leitfaden zum URL-Shortener-CLI denselben Erstellungsaufruf in curl und eine Shell-Funktion, und der MCP-Server-Leitfaden verbindet die API mit Claude, Cursor oder jedem anderen MCP-fähigen Client.
Analytics-Zugriff
Die Analytics-Endpunkte sind nur lesend und liegen unter /v1/workspaces/{id}/analytics/; der Leitfaden zur Link-Analytics-API listet jeden Report, seine Parameter und die Form der Antwort auf. Die häufigsten Abfragen:
GET .../clicks/recent?from=…&to=…- einzelne Klicks, neueste zuerst, seitenweise übernext_cursor. Nützlich für Export-Pipelines.GET .../timeseries?from=…&to=…&interval=day- in Intervalle gruppierte Klickzahlen;intervalisthouroderday, undtzlegt die Zeitzone der Intervalle fest.GET .../breakdown/country?from=…&to=…- geografische Aufschlüsselung.GET .../breakdown/referrer?from=…&to=…- Aufschlüsselung nach Referrer.
Die übrigen Reports sind summary, links/top, die restlichen Aufschlüsselungen (host, device, browser, destination) und die Toplisten (top-countries, top-regions, top-cities, top-referrers, top-destinations). from und to sind Daten im Format YYYY-MM-DD, wobei to exklusiv ist; ergänzen Sie link_id, um einen Report auf einen Link einzugrenzen, und limit, um Aufschlüsselungen und Toplisten zu dimensionieren.
Der Rohdaten-Feed der Klicks ist der größte. Ein Workspace mit 10M Klicks im Monat erzeugt etwa 600MB JSON an Rohereignissen im Monat, lesen Sie ihn also nach Zeitplan aus, statt in einem Rutsch.
Webhooks für Link-Ereignisse
Webhooks sind das Gegenstück zum Polling: Statt die API zu fragen, was sich geändert hat, bekommen Sie Link- und Domain-Ereignisse an Ihren Endpunkt zugestellt. Richten Sie sie unter /dashboard/webhooks mit einer Endpunkt-URL, den gewünschten Ereignissen (link.created, link.updated, link.expired und weitere) und einem Signing Secret ein.
Ein click.created-Ereignis pro Klick steht auf der Roadmap und ist noch nicht verfügbar, Klickdaten kommen also vorerst aus den Analytics-Endpunkten. Jede Zustellung trägt einen X-Elido-Signature-Header (auch als X-Webhook-Signature gesendet) mit dem Wert v1=<hex>: ein HMAC-SHA256, mit dem Secret Ihres Endpunkts als Schlüssel, über den X-Webhook-Timestamp-Wert, einen Punkt und den rohen Request-Body. Prüfen Sie ihn vor der Verarbeitung, sonst kann jeder Aufrufer an Ihren Endpunkt posten und sich als Elido ausgeben; der Leitfaden zum Verifizieren von Webhook-Signaturen zeigt die Prüfung in mehreren Sprachen.
Die Zustellung ist at-least-once. Fehlgeschlagene Zustellungen bekommen insgesamt drei Versuche im Abstand von Minuten. Der Vergleich von Webhooks und Polling wägt die beiden Integrationsmuster ab.
Ein Praxisbeispiel: Kampagnenautomatisierung
Die Integration, die die meisten API-Einführungen motiviert, sieht so aus. Ihre Marketing-Automatisierung legt in Customer.io oder HubSpot eine Kampagne an, und beim Veröffentlichen feuert ein Hook. Ihr Handler erstellt den Kurzlink. Dann speichert er ihn an der Kampagne und gibt ihn für die E-Mail-Vorlage zurück. 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;
}
Der Idempotency-Key stammt aus der Kampagnen-ID. Feuert der Publish-Hook zweimal (er wird es tun, denn Zustellungen sind at-least-once), liefert der zweite Aufruf denselben Link zurück statt eines Duplikats. Die Tags campaign: und batch: sind Ihre eigenen Join-Schlüssel. Klick-Reports lassen sich darüber der Kampagne zuordnen. Für die durchgängige Attribution mit UTM-Vorlagen und Conversion-Weiterleitung führt der Leitfaden zum UTM-Tracking durch die gesamte Pipeline.
Was die API noch nicht kann
Einiges, wonach Teams fragen, gibt es heute nicht:
- Link-Analytics mit einem Aufruf. Eine einzelne Anfrage, die alle Aufschlüsselungen eines Links zurückgibt, gibt es nicht; heute fragen Sie Klicks, Land, Referrer, Gerät und Zeitreihe getrennt ab, führen Sie sie also in Ihrem eigenen Code parallel aus.
- Webhook-Replay über die API. Das Dashboard zeigt den Zustellverlauf und kann wiederholen; die API noch nicht.
- Slug-Umbenennungen mit einer 301. Dasselbe gilt für die Felder
utmundmetadatabeim Erstellen; solange es sie nicht gibt, gehören UTM-Parameter ins Ziel und Join-Schlüssel in die Tags.
Meine Faustregel: Steht etwas in der OpenAPI-Spezifikation, bauen Sie darauf. Steht es nur in einem Blogbeitrag, auch in diesem, behandeln Sie es als geplant.
Weiterlesen im Blog
Dieser Hub steht im Features-Cluster neben Smart Links erklärt, das beschreibt, wie die Weiterleitungs-Engine einen Link am Edge auflöst.
- URL-Shortener-API: Rate Limits, Retries und Idempotenz - die Integration für Produktionsverkehr härten.
- URL-Shortener für Entwickler - was Sie in einer API prüfen sollten, bevor Sie darauf aufbauen.
- Serverseitiges Conversion-Tracking über Kurzlinks - die API um die Weiterleitung von Conversions erweitern.
- Wie Sie eine URL ohne Code kürzen - die Wege über Dashboard, Smartphone und Tabelle.
- Produktoberfläche:
/features/api-sdksund/solutions/developers.
Häufig gestellte Fragen
Wie kürze ich eine URL mit einer API?
Senden Sie einen authentifizierten POST mit der langen URL in einem JSON-Body und lesen Sie den Kurzlink aus der Antwort. Bei Elido ist das POST https://api.elido.app/v1/links mit dem Header Authorization: Bearer, gefolgt von Ihrem API-Schlüssel, und dem Body {"destination_url": "..."}; die Antwort 201 enthält short_url. Jede Sprache in diesem Leitfaden ist nur ein Wrapper um diese eine Anfrage.
Wie kürze ich eine URL in Python?
Rufen Sie requests.post gegen den links-Endpunkt auf, mit Ihrem Schlüssel im Header Authorization: Bearer, json={"destination_url": url} und einem timeout, rufen Sie dann raise_for_status() auf und lesen Sie response.json()["short_url"]. Für Tausende URLs wechseln Sie auf httpx.AsyncClient mit einem asyncio.Semaphore, damit nur eine Handvoll Anfragen gleichzeitig unterwegs ist.
Wie kürze ich eine URL in JavaScript oder Node.js?
Rufen Sie fetch ab Node 18 mit der Methode POST, dem Bearer-Header und einem JSON-Body auf, prüfen Sie res.ok, bevor Sie den Body liest, und nehmen Sie short_url aus dem geparsten JSON. Führen Sie das auf einem Server oder in einer Serverless-Route aus, nie im Browsercode, denn ein Schlüssel, der auf die Seite gelangt, ist für jeden lesbar, der die DevTools öffnet.
Brauche ich ein SDK, um eine URL-Shortener-API zu nutzen?
Nein. Einen Link zu erstellen ist ein einzelner POST mit zwei Headern, und das schafft der Standard-HTTP-Client jeder Sprache: requests oder httpx, fetch, net/http, java.net.http, HttpClient, curl, Net::HTTP und Invoke-RestMethod. Ein SDK lohnt sich, wenn Sie viele Endpunkte nutzen und typisierte Modelle wollen, aber die einfachen HTTP-Varianten in diesem Leitfaden unterstützen wir heute für den Produktivbetrieb.
Wie verhindere ich, dass Retries doppelte Kurzlinks erzeugen?
Senden Sie einen Idempotency-Key-Header und halten Sie ihn bei allen Wiederholungen derselben logischen Anfrage gleich, zum Beispiel als Hash des Ziels oder als Ihre Bestell-ID. Elido speichert die erste erfolgreiche Antwort 24 Stunden pro Workspace und spielt sie bei einem wiederholten Schlüssel erneut aus, sodass ein Retry nach einer verlorenen Antwort den ursprünglichen Link zurückgibt, statt einen zweiten zu erzeugen.
Wie viele Kurzlinks pro Sekunde kann ich über die API erstellen?
Elido begrenzt den API-Verkehr pro Workspace mit einem Token Bucket: 10 Anfragen pro Sekunde mit einem Burst von 20 bei Free, 30 mit einem Burst von 60 bei Pro und 100 mit einem Burst von 200 bei Business. Eine 429-Antwort enthält Retry-After: 1. Für große Mengen nimmt POST /v1/links/bulk bis zu 100 Links in einer Anfrage entgegen.
Elido testen
URL einfügen, kurzer Link in Sekunden
Kein Konto nötig. Link bleibt 30 Tage aktiv. Konto erstellen, um ihn dauerhaft zu behalten.
Keine Anmeldung nötig. Kostenlos registrieren für 50 Links im Monat.