Para acortar una URL con una API, envía un único POST autenticado: pon la URL larga en un cuerpo JSON como destination_url, mándalo a https://api.elido.app/v1/links con Authorization: Bearer <your key> y lee short_url en la respuesta 201 Created. Esa es toda la integración en cualquier lenguaje. Los ocho lenguajes de abajo solo se diferencian en cómo escriben la petición. Y en qué detalle de producción muerde primero.
La primera llamada lleva cinco minutos. Los problemas empiezan después, cuando llega el tráfico real y un gestor de reintentos crea duplicados: el panel se llena de variantes /foo-1, /foo-2 y /foo-3 de un mismo destino y alguien abre un ticket. Esta guía es el recorrido por la API de acortador de URL que ojalá hubiera leído ese equipo: autenticación, primera llamada, los cuatro endpoints de enlaces, claves de idempotencia, códigos de error, límites de tasa y, después, la misma petición de enlace corto en ocho lenguajes con el único hábito que necesita cada entorno de ejecución.
¿Aún estás eligiendo servicio? La comparativa de APIs de acortador de URL gratuitas pone lado a lado las formas de petición y los límites de los planes gratuitos entre proveedores. Todo lo que sigue se refiere a la API REST de Elido.
Salta directo a tu lenguaje:
- Python:
requestspara un enlace,httpxcon un semáforo para miles. - JavaScript y Node.js:
fetch, por qué la clave se queda fuera del navegador, un pool de workers en lugar dePromise.all. - Go: un
http.Clientcompartido con timeout,errgroup.SetLimitpara lotes. - Java: el
HttpClientdel JDK, hilos virtuales tras unSemaphore. - C# y .NET:
PostAsJsonAsyncy después un cliente tipado deIHttpClientFactory. - PHP, Laravel y WordPress: curl,
Http::con reintentos,wp_remote_postal publicar. - Ruby:
Net::HTTP.posty luego Faraday con un middleware de reintentos que sí reintenta un POST. - PowerShell:
Invoke-RestMethod, una clave en una bóveda y una columna entera de CSV.
Requisitos previos: una clave de API y una URL base
Inicia sesión en el panel, abre /dashboard/api-keys y crea una clave de API. Las claves empiezan por elido_ y se muestran una sola vez. Cada clave está ligada a un espacio de trabajo: una clave emitida en el espacio A no puede crear enlaces en el espacio B, y la API deduce a qué espacio te refieres solo por la clave. Los tokens de usuario máquina (para sistemas de CI, herramientas internas e integraciones de máquina a máquina) viven en /dashboard/machine-users y rotan de forma independiente de las claves personales.
Ambos tipos llevan un rol de espacio de trabajo predefinido (viewer, editor o admin) en lugar de permisos por endpoint, así que a un trabajo de CI que solo crea enlaces le corresponde editor. La guía de permisos de claves de API lista a qué accede cada rol. Los cambios en webhooks exigen admin, y explica por qué.
La URL base es https://api.elido.app y las rutas de enlaces cuelgan de /v1. Los dominios de redirección (f.elido.me en Free, s.elido.me en planes de pago, b.elido.me en Business) son otra superficie: tus enlaces cortos se resuelven ahí, mientras que la API los crea y los edita. La especificación OpenAPI 3.1 que hay detrás de la referencia de la API es el contrato. Si un campo está en la especificación, tiene soporte.
Un detalle pilla a las cuentas nuevas. Las escrituras hechas con una clave cuyo propietario no ha verificado su correo devuelven 403 (email_not_verified). Las lecturas siguen funcionando. Los tokens de usuario máquina están exentos. Verifica la dirección una vez y el error desaparece.
La primera llamada: crear un enlace corto
Esta es la petición que reproduce cada sección de lenguaje de abajo, en 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 respuesta es un 201 Created con el registro del enlace. Recortado a los campos que usarás:
{
"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"
}
Guarda id si alguna vez vas a editar, caducar o borrar el enlace: es el identificador canónico, y los slugs pueden cambiar mientras que los ID no. No pasaste domain_id, así que la API usó el dominio corto por defecto de tu plan. El slug lo genera el servidor salvo que envíes slug; los slugs generados son base62 ([0-9A-Za-z]) y de seis caracteres como máximo.
Guarda la clave en una variable de entorno (ELIDO_API_KEY en todos los ejemplos de aquí), nunca en el código fuente: la clave de un acortador es una credencial que gasta cuota y quien la tenga puede crear enlaces con la tuya, algo que importa sobre todo en JavaScript, como explica la sección de Node.
Los cuatro endpoints que de verdad usarás
La API tiene más de cuatro endpoints, pero la mayoría de las integraciones se quedan dentro de este conjunto. Las rutas planas /v1/links infieren el espacio de trabajo de tu clave; las rutas canónicas bajo /v1/workspaces/{workspace_id}/links se comportan igual y piden el ID explícito.
Crear un enlace
POST /v1/links toma destination_url más campos opcionales:
slug: un final de enlace que eliges tú; debe ser único en el dominio.domain_id: para un dominio personalizado; omítelo para usar el dominio corto por defecto de tu plan.titleytags: una etiqueta del panel y cadenas libres por las que filtrar después.expires_at: una marca de tiempo RFC 3339 a partir de la cual el enlace deja de redirigir y muestra una página de caducado.redirect_status:301,302(el valor por defecto) o307.
Las contraseñas no se fijan al crear: envía un PATCH justo después y la redirección mostrará una página de contraseña antes de reenviar. Los parámetros UTM van directamente en destination_url, y tus propias claves de unión en tags.
El slug personalizado es el campo que muerde a los equipos en producción. Reutiliza un slug en el mismo dominio y obtienes 409. Un gestor ingenuo que añade un contador (my-slug-1, my-slug-2) es justo como empieza el lío de enlaces duplicados del inicio. La solución es una clave de idempotencia, que se explica dos secciones más abajo.
Leer y listar enlaces
GET /v1/links/{id} devuelve el registro completo, con short_url. Los recuentos de clics no están en el registro del enlace. Salen de los endpoints de analítica descritos en la guía de la API de analítica de clics.
GET /v1/links?limit=…&tags=…&host=… lista enlaces con filtros (q busca, status y folder acotan más). La paginación es por cursor: devuelve el next_cursor de la respuesta como cursor y para cuando sea null.
Actualizar un enlace
PATCH /v1/links/{id} acepta los campos de creación. Las actualizaciones habituales son un nuevo destination_url (rotación de campañas sin reimprimir un código QR), nuevas etiquetas y un expires_at más tardío. Enviar un slug nuevo renombra el enlace. El slug antiguo muere al instante. No renombres nada que ya esté impreso.
Borrar y restaurar un enlace
DELETE /v1/links/{id} devuelve 204 No Content y manda el enlace a la papelera: deja de redirigir y desaparece de las llamadas de listado y lectura. Aún no se ha ido. POST /v1/links/{id}/restore lo recupera y GET /v1/links/trash muestra lo que hay. A los 30 días la papelera se vacía para siempre.
Claves de idempotencia: reintentos sin enlaces duplicados
Cada POST, PUT y PATCH acepta una cabecera Idempotency-Key, una cadena opaca que eliges tú. La primera vez que llega una clave con una respuesta correcta, el servidor guarda el código de estado y el cuerpo durante 24 horas, acotado a (workspace, key). Una repetición de la misma clave con la misma petición recibe la respuesta guardada, de modo que un reintento devuelve el enlace que ya existe en lugar de crear uno segundo.
Tres detalles deciden si te funciona:
- Solo se guardan las respuestas correctas. Un
429o un500no se cachea, así que el siguiente reintento con la misma clave obtiene un segundo intento real. - El servidor toma la huella del método, la ruta y el cuerpo. Reutiliza una clave con otro cuerpo y recibes
422conidempotency_key_mismatch, que es un fallo del llamador, no algo que reintentar. DELETEno admite clave; borrar dos veces es idempotente por naturaleza.
La clave en sí debe salir de algo estable. Derívala del evento de negocio (order-12345-link) cuando exista, o de un hash SHA-256 de la URL de destino en un trabajo por lotes. Un UUID aleatorio por intento anula el propósito. Uno por URL funciona hasta que se repite el lote.
La ventana de 24 horas es la trampa. Un reintento el tercer día de un trabajo atascado crea un enlace nuevo, así que en lotes de varios días guarda el id de la primera creación correcta y búscalo antes de volver a emitir. Las claves también son por espacio de trabajo: la misma clave en dos espacios crea dos enlaces, lo cual es correcto para una API multiespacio y aun así sorprende a los equipos que daban por hecho que las claves eran globales.
Una clave basada en el hash del destino tiene un efecto secundario que conviene conocer. Dos campañas que apunten a la misma URL en 24 horas comparten un enlace. Si quieres dos enlaces, mete el ID de campaña en la clave.
Gestión de errores y códigos de estado
Los errores llegan como un pequeño objeto JSON con un error legible y, en algunos casos, un code legible por máquina:
{ "error": "destination_url is required" }
Los códigos de estado que encontrarás:
400: fallo de validación, como undestination_urlausente, unexpires_atmal formado o unredirect_statusno válido. Corrige la carga; no reintentes.401: la clave falta, está revocada o mal escrita. Comprueba que la cabecera diceBearercon un espacio antes de la clave.402: se agotó la cuota de enlaces de tu plan. Reintentar no ayuda; mejorar de plan o esperar al siguiente periodo sí.403: el rol de la clave no permite la acción (una claveviewerno puede crear enlaces), el correo de la cuenta no está verificado o el análisis de seguridad bloqueó el destino ("code": "destination_blocked").404: el enlace no existe o esta clave no puede verlo. Devolvemos 404 en lugar de 403 para que un llamador no pueda sondear recursos que no son suyos.409: el slug ya está en uso en ese dominio.422: una clave de idempotencia reutilizada con otro cuerpo, o una combinación de opciones no permitida.429: alcanzaste el límite de tasa. EsperaRetry-Aftery vuelve a intentarlo con la misma clave de idempotencia.500,502,503,504: un fallo del servidor o transitorio. Espera con backoff y reintenta con la misma clave.
Esa división es toda la política de reintentos, y cada sección de lenguaje de abajo la implementa igual: reintentar 429 y 5xx, rendirse con todo lo demás. Reintentar un 400 con la misma carga no puede dar otra respuesta, y reintentar un 401 tres veces es solo un fallo más lento. La guía de límites de tasa y reintentos profundiza en el jitter y en por qué los bucles de reintento ciegos convierten una caída en dos.
Límites de tasa y el endpoint de lotes
El tráfico de la API se mide por espacio de trabajo con un token bucket:
| Plan | Tasa sostenida | Ráfaga |
|---|---|---|
| Free | 10 req/s | 20 |
| Pro | 30 req/s | 60 |
| Business | 100 req/s | 200 |
Un cubo lleno te deja enviar la ráfaga de golpe. Después te asientas en la tasa sostenida. Cada clave de API tiene además su propio cubo encima del del espacio de trabajo, así que un solo script descontrolado no puede gastar por sí solo toda la cuota del espacio.
Una petición rechazada recibe 429, una cabecera Retry-After: 1 y una cabecera X-RateLimit-Scope que indica qué cubo se vació (workspace o api_key). No hay cabeceras de cuota restante con las que dosificar. Así que el enfoque práctico es un techo fijo de concurrencia más un reintento en 429: ocho peticiones en vuelo son un buen punto de partida en Pro, y cuatro van bien en Free. Una rareza me pilló. El cuerpo del 429 es texto con forma de JSON enviado como text/plain, así que un cliente que solo analiza tipos de contenido JSON verá una cadena simple donde esperaba un objeto.
¿Un trabajo grande? POST /v1/links/bulk toma {"links": [...]} con hasta 100 elementos y cuenta como una petición contra el límite. Responde 200 aunque fallen algunos elementos, con contadores ok y failed y un status por elemento de ok o error, así que lee cada elemento. No te lo saltes. Los enlaces que devuelve llevan un slug pero no short_url, lo que significa que construyes la URL tú con tu dominio y el slug. El tutorial de importación masiva desde Google Sheets mueve así una hoja de cálculo entera.
Acorta una URL en tu lenguaje: la forma común
Cada sección a partir de aquí envía la petición de la primera llamada y aplica los mismos cuatro hábitos: la clave tomada del entorno, un timeout en cada petición, el código de estado comprobado antes de analizar el cuerpo y una Idempotency-Key estable en todo lo que pueda ejecutarse dos veces. No los voy a reexplicar por lenguaje. Lo que cambia es el idioma. Cada entorno de ejecución tiene además un detalle que la gente hace mal.
Cada sección abre con la llamada mínima que funciona. Eso basta para un script que lanzas a mano. Después viene la pieza de producción que más preguntan los usuarios de ese entorno: concurrencia en Python, Go o Java; vida útil del cliente en C#; alojamiento en PHP; middleware en Ruby; secretos y CSV en PowerShell. ¿Quieres ejecutarlos tal cual? Crea una clave en el plan gratuito, expórtala como ELIDO_API_KEY y cada fragmento funciona sin cambios.
Acortar una URL en Python
En Python, acortar una URL es un requests.post con la clave en una cabecera Bearer, el destino en json= y 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 biblioteca requests no tiene timeout por defecto, así que sin timeout=10 una conexión colgada bloquea el script para siempre. raise_for_status() convierte un 401 o un 500 en una excepción en lugar de dejar que una llamada fallida parezca un éxito. Si no puedes instalar nada con pip, urllib.request, de la biblioteca estándar, envía el mismo POST con más ceremonia.
Lotes con httpx y un semáforo
Un bucle sobre requests.post sirve para unos cientos de enlaces. Para decenas de miles, el tiempo se va esperando a la red, y httpx con asyncio solapa esas esperas. El semáforo es lo que más importa. Limita las peticiones en vuelo, que es la diferencia entre un lote rápido y un muro de 429 del que el bucle de reintentos tiene que salir luego a pulso.
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))
Los resultados se indexan por la URL original. Un fallo parcial queda a la vista. Como la clave es un hash del destino, repetir toda la lista tras arreglar tres filas malas no crea nada dos veces. Dormir dentro del semáforo es deliberado: un worker al que se le ha dicho que espere no debe ceder su hueco a una petición nueva.
Acortar una URL en JavaScript y Node.js
En JavaScript, acortar una URL es un POST con fetch. Node trae fetch como global desde la versión 18, así que no hay nada que instalar:
const res = await fetch("https://api.elido.app/v1/links", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ELIDO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ destination_url: "https://example.com/spring-sale" }),
signal: AbortSignal.timeout(10_000),
});
if (!res.ok)
throw new Error(`shorten failed: ${res.status} ${await res.text()}`);
const { short_url } = await res.json();
console.log(short_url);
Ejecútalo con node --env-file=.env shorten.mjs. Esa línea if (!res.ok) no es decoración: fetch solo rechaza ante un fallo de red o un aborto, así que un 401 llega como una promesa resuelta con ok: false. A los usuarios de Axios esto les muerde una vez. Normalmente se nota como un undefined donde debería haber un enlace.
Mantén la clave en el servidor
Pega ese fragmento en un componente de React y se rompen dos cosas. CORS bloquea la respuesta, porque una API autenticada con clave no envía Access-Control-Allow-Origin para sitios arbitrarios. El problema mayor está detrás del error: una clave en el código del front-end la puede leer cualquiera que abra DevTools, y un prefijo NEXT_PUBLIC_ la anuncia. Así que el navegador envía una URL simple a tu ruta. La ruta añade la clave.
Valida la URL que acepta tu ruta, o habrás construido una redirección abierta con pasos extra; el artículo sobre vulnerabilidades de redirección abierta muestra qué hacen los atacantes con una.
Para un lote, resiste la tentación de Promise.all(urls.map(shorten)). Dispara todas las peticiones en el mismo tick, así que 500 URL se convierten en 500 conexiones simultáneas y la mayoría vuelve con 429. Un pequeño pool de workers que tiran de una cola compartida mantiene la concurrencia plana por larga que sea la 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;
}
En runtimes edge (Vercel Edge, Cloudflare Workers, Deno) puede faltar node:crypto; calcula el hash con crypto.subtle.digest("SHA-256", …) y el resto no cambia.
Acortar una URL en Go
En Go, acortar una URL pide net/http y encoding/json de la biblioteca estándar: serializar el cuerpo, fijar dos cabeceras y decodificar short_url. Escríbelo desde el principio como una función sobre un cliente compartido, porque es la versión que te vas a quedar:
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 y http.Post no tienen timeout. Ninguno. Un host que no responde puede retener una goroutine durante minutos. Un cliente único a nivel de paquete mantiene vivo el pool de conexiones entre llamadas, y NewRequestWithContext hace que una CLI cancelada o un llamador desconectado detenga la petición en lugar de dejarla corriendo. Si un lote abre muchos más sockets de los esperados, sube MaxIdleConnsPerHost en el transporte: su valor por defecto de 2 pasa desapercibido en un script y sale caro cuando ocho goroutines comparten un host.
Fan-out acotado con errgroup.SetLimit
Una goroutine por URL es barato para Go y ruinoso para el límite de tasa: 5.000 URL se convierten en 5.000 peticiones a la vez. errgroup con SetLimit te da un fan-out con techo.
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
}
Esto usa un errgroup.Group simple a propósito. errgroup.WithContext cancela todo al primer error, lo que va bien para un paso de build de todo o nada y es justo lo contrario de lo que necesita un importador que debe acortar lo que pueda, seguir de largo ante las filas malas e informar de los fallos al final. Desde Go 1.22 la variable de bucle es por iteración, así que la vieja línea u := u ya no hace falta, pero el mutex no es opcional: go test -race marcará un mapa sin protección. Añade un reintento en 429 y 5xx dentro de shorten usando select sobre ctx.Done() y time.After, para que el backoff siga siendo interrumpible.
Acortar una URL en Java
En Java, acortar una URL es un POST con java.net.http.HttpClient, que está en el JDK desde Java 11, así que la primera versión no necesita ninguna dependencia de 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"
}
Dos timeouts hacen dos trabajos. connectTimeout acota el apretón de manos TCP y TLS; el timeout de la petición acota todo el intercambio, y sin él un servidor que acepta la conexión y luego calla retiene el hilo. send solo lanza excepción ante fallos de transporte y timeouts, nunca ante un error HTTP, por eso la comprobación del estado es el contrato y no programación defensiva. Construye un solo HttpClient por aplicación: es seguro entre hilos y tiene un pool de conexiones, y crear uno por llamada repite el apretón de manos TLS cada vez.
Hilos virtuales tras un semáforo
Los hilos virtuales quedaron definitivos en Java 21. Hacen que un hilo por URL sea lo bastante barato como para ser sensato. El límite de tasa, en cambio, no se ha movido, así que el Semaphore hace el trabajo de verdad. Aquí se analiza con Jackson. Una cadena JSON armada a mano se rompe con el primer destino que contenga una comilla.
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;
}
El bloque try-with-resources hace un trabajo real: cerrar un ejecutor de hilos virtuales espera a todas las tareas enviadas, así que no hace falta el baile con awaitTermination. En Java 11 a 17, la misma forma funciona con sendAsync y un pool fijo. Lo que falla en cualquier versión es CompletableFuture.allOf sobre una lista sin límite, que lo arranca todo a la vez y recoge un montón de 429.
Acortar una URL en C# y .NET
En C#, acortar una URL es una llamada a PostAsJsonAsync sobre un HttpClient que ya lleva la cabecera 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 es la línea que importa. Sin ella, un 401 termina tan tranquilo, ReadFromJsonAsync te devuelve un record lleno de nulos y el fallo aflora tres capas más allá como un NullReferenceException. El objeto anónimo conserva el nombre de campo en snake_case sin política de nombres; en un proyecto real, fija una vez JsonNamingPolicy.SnakeCaseLower en las opciones del serializador.
Un cliente tipado de IHttpClientFactory
Ese fragmento está bien en una prueba de consola y mal en un servicio. Un HttpClient creado por llamada abre un pool de conexiones nuevo cada vez, y desecharlo deja sockets en TIME_WAIT; hazlo en un bucle y te quedas sin puertos. IHttpClientFactory agrupa los handlers y los rota para que se recojan los cambios de DNS.
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 viene de Microsoft.Extensions.Http.Resilience y aporta reintentos con jitter en 429 y 5xx, timeouts y un cortacircuitos. Reintentará un POST salvo que le digas que no, y la cabecera Idempotency-Key es lo que lo hace seguro. Leer el cuerpo del error antes de lanzar es la otra mitad: EnsureSuccessStatusCode se queda solo con el código de estado, y a las 3 de la madrugada te deja adivinando a qué campo puso objeciones el 400. Para lotes, Parallel.ForEachAsync con MaxDegreeOfParallelism = 8 limita las peticiones en vuelo y se lee mejor que un semáforo hecho a mano. Guarda la clave en los secretos de usuario en local y en variables de entorno o una bóveda en producción, nunca en un appsettings.json versionado.
Acortar una URL en PHP, Laravel y WordPress
En PHP, acortar una URL funciona con la extensión curl que viene con PHP, así que corre incluso en un alojamiento compartido donde Composer no es una opción:
<?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'];
Dos líneas existen por fallos con los que todos tropiezan una vez. Sin CURLOPT_RETURNTRANSFER, curl imprime el JSON en la salida y devuelve true, que es la historia real tras la mayoría de las preguntas de «mi respuesta está vacía». Y CURLOPT_POSTFIELDS cambia a codificación de formulario multipart en cuanto le pasas un array, así que json_encode es lo que lo convierte en una petición JSON. Muchos tutoriales de PHP todavía llaman al urlshortener/v1 de Google o al endpoint v3 de bit.ly; ambos hace tiempo que no existen, y el artículo sobre la alternativa al acortador de URL de Google cuenta adónde fueron los enlaces goo.gl.
El cliente Http de Laravel con reintentos
En una aplicación Laravel o Symfony, el cliente HTTP de Laravel (una envoltura de Guzzle) declara la política de reintentos una sola vez:
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.elido.key'))
->withHeaders(['Idempotency-Key' => hash('sha256', $destination)])
->timeout(10)
->retry(3, 1000, function (Throwable $e) {
return $e instanceof ConnectionException
|| ($e instanceof RequestException
&& in_array($e->response->status(), [429, 500, 502, 503, 504], true));
}, throw: false)
->post('https://api.elido.app/v1/links', ['destination_url' => $destination]);
$short = $response->successful() ? $response->json('short_url') : null;
El callback when es lo que evita que un 401 se reintente tres veces. Guzzle simple se comporta distinto de curl: por defecto lanza ClientException en 4xx y ServerException en 5xx, así que captúralas o pon http_errors en false. Para una importación nocturna, el Pool de Guzzle con 'concurrency' => 8 es la respuesta de PHP al paralelismo acotado. Una trampa propia de PHP: sleep() dentro de una petición web retiene un worker de php-fpm todo ese tiempo, así que los reintentos pertenecen a scripts de CLI, trabajos de cola y WP-Cron.
WordPress: wp_remote_post al publicar
WordPress tiene su propia capa HTTP, wp_remote_post, que elige un transporte y funciona en alojamientos donde curl está desactivado. Engánchala a la publicación y guarda el resultado como meta de la entrada:
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']);
});
Define ELIDO_API_KEY en wp-config.php, no en un archivo de plugin que acaba en un repositorio público ni en una fila de opciones que puede leer cualquier administrador. La clave post-123 es estable. Dispara el hook dos veces y la segunda llamada devuelve el enlace existente. Si prefieres no mantener ningún hook, la guía del acortador de URL para WordPress compara esto con el plugin y las vías sin código.
Acortar una URL en Ruby
En Ruby, acortar una URL es un Net::HTTP.post de la biblioteca estándar, así que un script no necesita ninguna gema:
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 en lugar de ENV[] falla en el acto cuando falta la clave, en vez de enviar Bearer y volver con un 401 desconcertante. La comprobación is_a?(Net::HTTPSuccess) es la línea que la gente se salta: Net::HTTP lanza excepción ante sockets muertos y timeouts, y ante nada más. Net::HTTP.post tampoco acepta argumentos de timeout, así que para cualquier cosa desatendida baja a Net::HTTP.start con open_timeout y read_timeout.
Faraday con middleware de reintentos
Dentro de una aplicación Rails, Faraday te da un único objeto de conexión con la cabecera de autenticación puesta, JSON en ambos sentidos y una política de reintentos declarada. Desde Faraday 2, el middleware de reintentos vive en la gema aparte 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 línea methods: %i[post] es la que omiten la mayoría de los fragmentos de Faraday que hay en línea. De fábrica, faraday-retry solo reintenta verbos idempotentes (GET, PUT, DELETE, HEAD, OPTIONS), así que una configuración de POST sin ella nunca reintenta nada y nadie lo nota hasta que un 503 pierde un enlace. Activar POST es seguro aquí justamente por la Idempotency-Key. El middleware también respeta Retry-After en un 429. Para una lista, una Queue y ocho hilos funcionan bien incluso en CRuby, porque el bloqueo global se libera durante la E/S; en Rails, envuelve shorten en un Active Job para que un reintento dormido no retenga un hilo de Puma.
Acortar una URL en PowerShell
En PowerShell, acortar una URL es una llamada a Invoke-RestMethod, integrada en Windows PowerShell 5.1 y en PowerShell 7, que devuelve un objeto ya analizado en lugar de una cadena 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 no es opcional. Pasa un hashtable directo a -Body y PowerShell lo codifica como formulario, y obtienes un 400 que parece un problema del servidor. La sorpresa para quien viene de otros lenguajes va en sentido contrario: un 401 o un 404 aquí es un error terminante, no un valor devuelto, así que capturas una excepción en lugar de comprobar un código de estado. PowerShell 7 añade -SkipHttpErrorCheck, que devuelve la respuesta en su lugar.
Una clave en una bóveda y una columna entera de CSV
Una clave escrita en un archivo .ps1 acaba en el control de versiones, en Get-History y en cualquier transcripción que el parque de equipos tenga activada. El módulo SecretManagement la guarda en una bóveda, de modo que rotar una clave es actualizar una entrada en vez de buscar entre los scripts de cuatro servidores. La tarea real habitual es una columna de hoja de cálculo más que una sola 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 ocho peticiones en vuelo tenga el archivo cincuenta filas o cincuenta mil. Cada iteración de -Parallel corre en su propio runspace, por lo que la función entra mediante $using:, y toda la construcción necesita PowerShell 7; en 5.1, un simple foreach es la respuesta honesta para unos cientos de filas. Los errores caen en la columna de salida, y como la clave de idempotencia sale del destino, repetir el archivo tras arreglar tres filas no crea duplicados. Comprueba unas cuantas filas con curl.exe -sI antes de que nada se mande a imprimir. ¿El CSV ya está en una hoja de Google? La importación masiva sin código hace el mismo trabajo.
Qué hacen hoy los SDK
Existen bibliotecas cliente oficiales para cuatro lenguajes (TypeScript como @elido/sdk, Python, Go y Ruby) y siguen la especificación OpenAPI. Solo el SDK de Go está publicado hasta ahora, como el módulo github.com/elidoapp/elido-go (v0.1.0). Los SDK de TypeScript, Python y Ruby todavía no se han publicado en npm, PyPI ni RubyGems. Por eso todos los ejemplos de arriba usan el cliente HTTP propio de cada lenguaje, que es además lo que respaldamos hoy en integraciones de producción. Cuando salgan las demás bibliotecas, la página de API y SDK llevará las instrucciones de instalación.
Cuando estén publicadas, espera que envuelvan las rutas con ámbito de espacio de trabajo, así que pasarás el ID del espacio y un domain_id de forma explícita, y que difieran en su comportamiento de reintentos. El cliente de TypeScript añade una Idempotency-Key a cada POST, PUT o PATCH y reintenta 502, 503 y 504 con backoff, dejándote 429 a ti. El cliente de Ruby también genera claves. Lanza un error tipado de límite de tasa con retry_after, pero nunca reintenta por sí solo. Los clientes de Python y Go no reintentan en absoluto. Uses el que uses, la política de reintentos de la sección de errores sigue aplicándose.
Para una terminal en lugar de una base de código, la guía de la CLI del acortador de URL envuelve la misma llamada de creación en curl y una función de shell, y la guía del servidor MCP conecta la API con Claude, Cursor o cualquier otro cliente compatible con MCP.
Acceso a la analítica
Los endpoints de analítica son de solo lectura y viven bajo /v1/workspaces/{id}/analytics/; la guía de la API de analítica de enlaces lista cada informe, sus parámetros y la forma de su respuesta. Las consultas más habituales:
GET .../clicks/recent?from=…&to=…: clics individuales, los más recientes primero, paginados connext_cursor. Útil para pipelines de exportación.GET .../timeseries?from=…&to=…&interval=day: recuentos de clics por intervalo;intervaleshouroday, ytzfija la zona horaria de los intervalos.GET .../breakdown/country?from=…&to=…: desglose geográfico.GET .../breakdown/referrer?from=…&to=…: desglose por referente.
Los demás informes son summary, links/top, el resto de desgloses (host, device, browser, destination) y las listas de los más altos (top-countries, top-regions, top-cities, top-referrers, top-destinations). from y to son fechas YYYY-MM-DD y to es exclusivo; añade link_id para acotar cualquier informe a un enlace, y limit para dimensionar desgloses y listas.
El flujo de clics en bruto es el más grande. Un espacio de trabajo con 10M de clics al mes produce unos 600 MB de JSON de eventos en bruto al mes, así que recórrelo de forma programada en lugar de de una sentada.
Webhooks para eventos de enlaces
Los webhooks son lo contrario del sondeo: en lugar de preguntar a la API qué ha cambiado, recibes los eventos de enlaces y dominios en tu endpoint. Se configuran en /dashboard/webhooks con una URL de endpoint, los eventos que quieras (link.created, link.updated, link.expired y otros) y un secreto de firma.
Un evento click.created por clic está en la hoja de ruta y todavía no está disponible, así que por ahora los datos de clics salen de los endpoints de analítica. Cada entrega lleva una cabecera X-Elido-Signature (que también se envía como X-Webhook-Signature) con el valor v1=<hex>: un HMAC-SHA256, con la clave del secreto de tu endpoint, sobre el valor de X-Webhook-Timestamp, un punto y el cuerpo en bruto de la petición. Verifícala antes de procesar, o cualquiera podrá enviar peticiones a tu endpoint y hacerse pasar por Elido; la guía para verificar firmas de webhook trae la comprobación en varios lenguajes.
La entrega es de al menos una vez. Las entregas fallidas reciben tres intentos en total, separados por minutos. La comparativa de webhooks y sondeo sopesa los dos patrones de integración.
Un ejemplo práctico: automatización de campañas
La integración que motiva la mayoría de las adopciones de API se parece a esto. Tu automatización de marketing crea una campaña en Customer.io o HubSpot y salta un hook al publicarla. Tu gestor crea el enlace corto. Luego lo guarda en la campaña y lo devuelve para la plantilla del correo. En 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 clave de idempotencia sale del ID de campaña. Cuando el hook de publicación se dispara dos veces (lo hará, porque las entregas son de al menos una vez), la segunda llamada recibe el mismo enlace en lugar de un duplicado. Las etiquetas campaign: y batch: son tus propias claves de unión. Los informes de clics se vinculan con la campaña a través de ellas. Para la atribución de extremo a extremo con plantillas UTM y reenvío de conversiones, la guía de seguimiento UTM recorre el pipeline completo.
Lo que todavía no está en la API
Algunas cosas que preguntan los equipos no están disponibles hoy:
- Analítica en una llamada. No existe una petición única que devuelva todos los desgloses de un enlace; hoy pides clics, país, referente, dispositivo y serie temporal por separado, así que lánzalas en paralelo desde tu propio código.
- Reenvío de webhooks. El panel muestra el historial de entregas y puede reenviar; la API todavía no.
- Renombrar con 301. Lo mismo ocurre con los campos
utmymetadataal crear; hasta que existan, pon los parámetros UTM en el destino y las claves de unión en las etiquetas.
Mi regla práctica: si está en la especificación OpenAPI, construye sobre ello. Si solo está en un artículo de blog, este incluido, trátalo como planeado.
Lecturas relacionadas en el blog
Este hub está en el clúster de funciones junto a los enlaces inteligentes explicados, que cuenta cómo el motor de redirección resuelve un enlace en el borde.
- API de acortador de URL: límites de tasa, reintentos e idempotencia: endurecer la integración para el tráfico de producción.
- Acortadores de URL para desarrolladores: qué comprobar en una API antes de construir sobre ella.
- Seguimiento de conversiones desde el servidor con enlaces cortos: extender la API hasta el reenvío de conversiones.
- Cómo acortar una URL sin código: las vías del panel, el móvil y la hoja de cálculo.
- Superficie de producto:
/features/api-sdksy/solutions/developers.
Preguntas frecuentes
¿Cómo acorto una URL con una API?
Envía un POST autenticado con la URL larga en un cuerpo JSON y lee el enlace corto de la respuesta. En Elido es POST https://api.elido.app/v1/links con la cabecera Authorization: Bearer seguida de tu clave de API y el cuerpo {"destination_url": "..."}; la respuesta 201 trae short_url. Todos los lenguajes de esta guía son una envoltura de esa única petición.
¿Cómo acorto una URL en Python?
Usa requests.post contra el endpoint de enlaces con tu clave en una cabecera Authorization: Bearer, json={"destination_url": url} y un timeout, llama a raise_for_status() y lee response.json()["short_url"]. Para miles de URL, pasa a httpx.AsyncClient con un asyncio.Semaphore para que solo haya un puñado de peticiones en vuelo a la vez.
¿Cómo acorto una URL en JavaScript o Node.js?
Llama a fetch desde Node 18 o posterior con el método POST, la cabecera Bearer y un cuerpo JSON, comprueba res.ok antes de leer el cuerpo y toma short_url del JSON analizado. Ejecútalo en un servidor o en una ruta serverless, nunca en código de navegador: una clave enviada a la página la puede leer cualquiera que abra DevTools.
¿Necesito un SDK para usar una API de acortador de URL?
No. Crear un enlace es un POST con dos cabeceras, y el cliente HTTP estándar de cada lenguaje puede enviarlo: requests o httpx, fetch, net/http, java.net.http, HttpClient, curl, Net::HTTP e Invoke-RestMethod. Un SDK compensa cuando usas muchos endpoints y quieres modelos tipados, pero las versiones con HTTP simple de esta guía son lo que damos soporte hoy en producción.
¿Cómo evito que los reintentos creen enlaces cortos duplicados?
Envía una cabecera Idempotency-Key y mantenla igual en cada reintento de una misma petición lógica, por ejemplo un hash del destino o el ID de tu pedido. Elido guarda la primera respuesta correcta durante 24 horas por espacio de trabajo y la repite para una clave repetida, así que un reintento tras una respuesta perdida devuelve el enlace original en lugar de crear un segundo.
¿Cuántos enlaces cortos puedo crear por segundo con la API?
Elido limita el tráfico de la API por espacio de trabajo con un token bucket: 10 peticiones por segundo con ráfaga de 20 en Free, 30 con ráfaga de 60 en Pro y 100 con ráfaga de 200 en Business. Una respuesta 429 lleva Retry-After: 1. Para lotes grandes, POST /v1/links/bulk acepta hasta 100 enlaces por petición.
Prueba Elido
Pega una URL, obtén un enlace corto
Sin registro. El enlace vive 30 días. Crea una cuenta para conservarlo.
Sin registro. Crea una cuenta gratis y obtén 50 enlaces al mes.