Una API de acortador de URLs es una de las integraciones más pequeñas en el backlog de un equipo de ingeniería típico. Tres endpoints, un encabezado de autenticación, un payload JSON. La página de documentación promete la primera llamada en cinco minutos. Luego llega el tráfico de producción, la lógica de reintento crea enlaces duplicados, el panel de control se llena de variantes /foo-1, /foo-2, /foo-3 del mismo destino, y alguien abre un ticket.
Este post recorre la integración real. Autenticación, la primera llamada, los cuatro endpoints que cubren la mayoría de los casos de uso, idempotencia, manejo de errores, límites de tasa y los problemas de producción que la guía rápida de cinco minutos omite. Ejemplos de código en TypeScript, Python, Go, Ruby y PHP: los tres primeros a través de los SDK oficiales (@elido/sdk, elido-python, github.com/elido/elido-go), los dos últimos mediante clientes HTTP estándar.
Requisitos previos#
Inicia sesión en el panel de control, navega a /settings/api y crea un token de acceso personal. Los tokens tienen alcance de espacio de trabajo (workspace): un token emitido en el espacio de trabajo A no puede crear enlaces en el espacio de trabajo B. Los tokens de cuenta de servicio (para sistemas de CI, herramientas internas, integración máquina a máquina) se crean en la misma pantalla en los planes Pro y superiores; tienen alcances (scopes) explícitos (links:write, analytics:read, domains:write) y rotan independientemente de los tokens personales.
La URL base es https://api.elido.app/v1. Los dominios de redirección (f.elido.me, s.elido.me, b.elido.me) son independientes de la superficie de la API. Tus enlaces cortos se resuelven en el dominio de redirección; la API sirve para crearlos, modificarlos y leerlos.
La especificación OpenAPI se publica en https://api.elido.app/v1/openapi.json y cumple con OpenAPI 3.1. Los SDK oficiales se generan a partir de esa especificación y se republican con cada lanzamiento de la API; también puedes generar tu propio cliente en cualquier lenguaje compatible con OpenAPI.
La primera llamada#
Crea un enlace corto a partir de la URL de destino. Cinco líneas en TypeScript:
import { Elido } from "@elido/sdk";
const elido = new Elido({ token: process.env.ELIDO_TOKEN! });
const link = await elido.links.create({
destinationUrl: "https://shop.example.com/spring-sale",
});
console.log(link.shortUrl); // https://s.elido.me/abc123
Python:
from elido import Elido
client = Elido(token=os.environ["ELIDO_TOKEN"])
link = client.links.create(
destination_url="https://shop.example.com/spring-sale",
)
print(link.short_url) # https://s.elido.me/abc123
Go:
import "github.com/elido/elido-go/v2/elido"
client := elido.NewClient(elido.WithToken(os.Getenv("ELIDO_TOKEN")))
link, err := client.Links.Create(ctx, &elido.LinkCreateInput{
DestinationURL: "https://shop.example.com/spring-sale",
})
if err != nil {
return fmt.Errorf("create link: %w", err)
}
fmt.Println(link.ShortURL)
Ruby (sin SDK oficial, usando net/http):
require "net/http"
require "json"
uri = URI("https://api.elido.app/v1/links")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV['ELIDO_TOKEN']}"
req["Content-Type"] = "application/json"
req.body = { destination_url: "https://shop.example.com/spring-sale" }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
link = JSON.parse(res.body)
puts link["short_url"]
PHP (Guzzle):
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.elido.app/v1/']);
$res = $client->post('links', [
'headers' => ['Authorization' => 'Bearer ' . getenv('ELIDO_TOKEN')],
'json' => ['destination_url' => 'https://shop.example.com/spring-sale'],
]);
$link = json_decode((string) $res->getBody(), true);
echo $link['short_url'];
Los cinco producen el mismo resultado. El cuerpo de la respuesta contiene la URL corta, el ID de enlace canónico, el ID del espacio de trabajo y la marca de tiempo de creación. El slug — abc123 en el ejemplo anterior — es generado por el servidor a menos que pases custom_slug en la solicitud. El alfabeto del slug es base62 ([0-9A-Za-z]); la longitud predeterminada es de seis caracteres.
Los cuatro endpoints que realmente utilizarás#
La API tiene más de cuatro endpoints, pero la mayoría de las integraciones se mantienen dentro de este conjunto.
Crear un enlace#
POST /v1/links acepta la URL de destino más campos opcionales:
custom_slug: un slug que elijas (debe ser único dentro del espacio de trabajo).domain_id: para enlaces de dominios personalizados; si se omite, se utiliza el dominio principal del espacio de trabajo.tags: un array de cadenas de texto libre para organización.utm: parámetros de campaña para añadir al destino en el momento de la redirección.expires_at: marca de tiempo ISO 8601 después de la cual el enlace devuelve 410 Gone.password: si se establece, la redirección muestra una página de contraseña antes de redirigir.metadata: objeto JSON opaco que la redirección no interpreta; útil para tus propias claves de unión.
El slug personalizado es el campo que causa problemas a los equipos en producción. Si pasas un slug que ya está en uso por otro enlace en el mismo espacio de trabajo, la API devuelve 409 Conflict. El controlador de reintentos ingenuo que añade un contador (my-slug-1, my-slug-2) produce el problema de enlaces duplicados descrito al principio. El comportamiento de reintento correcto se describe en la sección de idempotencia a continuación.
Leer un enlace#
GET /v1/links/{id} devuelve el registro completo del enlace, incluyendo el conteo actual de clics, la marca de tiempo del clic más reciente y toda la configuración. El ID del enlace es el identificador canónico: los slugs pueden cambiar (Pro+ admite cambios de nombre de slug), los ID no.
GET /v1/links?domain_id=…&tag=…&limit=… enumera los enlaces en el espacio de trabajo con filtros. La paginación se basa en cursores; el next_cursor en la respuesta es opaco y se devuelve como el parámetro de consulta cursor en la siguiente solicitud.
Actualizar un enlace#
PATCH /v1/links/{id} acepta los mismos campos que la creación. Las actualizaciones más comunes: cambiar la URL de destino (útil para la rotación de campañas sin volver a imprimir códigos QR), cambiar etiquetas, extender expires_at. Actualizar el slug es un endpoint POST /v1/links/{id}/rename independiente que gestiona la redirección 301 desde el antiguo slug durante un periodo de retención configurable (30 días por defecto).
Eliminar un enlace#
DELETE /v1/links/{id} realiza un borrado lógico (soft-delete). El enlace devuelve 410 Gone durante los siguientes 90 días, luego se borra permanentemente. La vista de papelera del panel de control muestra los enlaces eliminados lógicamente; puedes restaurarlos a través del panel de control o mediante POST /v1/links/{id}/restore dentro del plazo de 90 días.
Claves de idempotencia#
Cada solicitud de mutación (POST, PATCH, DELETE) acepta un encabezado Idempotency-Key. El valor del encabezado es una cadena opaca de hasta 255 caracteres; el servidor almacena el cuerpo de la respuesta y el código de estado durante 24 horas con la clave (workspace_id, idempotency_key) y devuelve la respuesta almacenada si se presenta la misma clave nuevamente.
Los SDK oficiales generan claves de idempotencia automáticamente cuando no se proporcionan. Puedes sobrescribirlas:
const link = await elido.links.create(
{ destinationUrl: "https://shop.example.com/spring-sale" },
{ idempotencyKey: "order-12345-link" },
);
El caso de uso es un bucle de reintento. Si tu trabajo crea un enlace como parte del procesamiento de un pedido previo, genera la clave de idempotencia a partir del ID del pedido. Un reintento del mismo trabajo verá la misma clave, accederá al caché de idempotencia y devolverá el enlace creado originalmente en lugar de producir un segundo enlace.
El problema clave: el caché de idempotencia vive 24 horas, no para siempre. Un reintento en el tercer día de un trabajo estancado creará un nuevo enlace. Si la integración se ejecuta en lotes de varios días, almacena el ID del enlace devuelto por la primera creación exitosa y búscalo antes de volver a emitir.
Un segundo problema: la idempotencia es por espacio de trabajo. La misma clave en dos espacios de trabajo crea dos enlaces. Esta es la semántica correcta para una API multi-espacio de trabajo, pero puede sorprender a los equipos que asumen que la clave es globalmente única.
Manejo de errores#
La API devuelve códigos de estado HTTP estándar más un cuerpo de error estructurado:
{
"error": {
"code": "rate_limit_exceeded",
"message": "Workspace rate limit of 100 req/s exceeded. Retry after 1 second.",
"request_id": "req_01HXYZAB123",
"retry_after": 1
}
}
Los códigos que verás con más frecuencia:
400 invalid_request: error de validación del payload. El campomessageenumera los campos específicos. No reintentes; corrige el payload.401 unauthorized: token ausente o inválido. No reintentes sin rotar el token.403 forbidden: el token no tiene el alcance requerido. Consulta la lista de alcances del token en/settings/api.404 not_found: el recurso no existe o el token no tiene acceso a él (devolvemos 404 en lugar de 403 para evitar filtrar la existencia de recursos a llamadores no autorizados).409 conflict: slug ya en uso, o se detectó una edición simultánea (PATCH en una versión obsoleta). Vuelve a obtenerlo e inténtalo de nuevo.429 rate_limit_exceeded: retrocede (back off) según el valor deretry_after.500 internal_server_error: fallo del lado del servidor. Es seguro reintentar con la misma clave de idempotencia.502 bad_gateway,503 service_unavailable,504 gateway_timeout: problemas transitorios de infraestructura. Retrocede y reintenta.
Los SDK oficiales implementan retroceso exponencial con jitter para 429, 500, 502, 503 y 504. No reintentan 400, 401, 403, 404 o 409; esos son errores de programación o conflictos de lógica de negocio, no fallos transitorios. Los clientes HTTP personalizados deben seguir el mismo patrón; reintentar un 400 con el mismo payload no producirá un resultado diferente.
El request_id en el cuerpo del error es el campo que debes incluir en los tickets de soporte. Podemos rastrear cualquier solicitud a partir de ese ID a través del registro de auditoría, el registro de la aplicación y las métricas de la plataforma, y no podemos rastrear una solicitud sin él.
Límites de tasa#
Los límites de tasa publicados son 100 solicitudes por segundo por espacio de trabajo en Pro, 500 en Business y un límite negociado en Enterprise. El nivel gratuito es de 10 req/s.
El estado del límite de tasa se expone en tres encabezados de respuesta en cada respuesta de la API:
X-RateLimit-Limit: el límite actual por segundo.X-RateLimit-Remaining: solicitudes restantes en el segundo actual.X-RateLimit-Reset: marca de tiempo Unix cuando se reinicia el cubo (bucket).
El límite de 100/s es una implementación de token-bucket con una capacidad de ráfaga (burst) de 200, lo que significa que puedes emitir 200 solicitudes a la vez si el cubo está lleno y luego estabilizarte en la tasa sostenida de 100/s. La mayoría de los trabajos de creación de enlaces cortos encajan cómodamente en la ráfaga; las integraciones con gran carga analítica que paginan a través de eventos de clics históricos se benefician del margen del nivel Pro.
Para operaciones masivas en Business+, el endpoint POST /v1/links/bulk acepta hasta 1000 enlaces por solicitud y cuenta como una unidad de límite de tasa. Este es el endpoint adecuado para cualquier trabajo que cree más de cien enlaces a la vez.
Lo que hacen los SDK que el HTTP estándar no hace#
Los SDK oficiales incluyen cuatro cosas que se amortizan rápidamente:
- Reintento automático con retroceso para los códigos de estado reintentables.
- Generación de claves de idempotencia cuando no se proporcionan explícitamente.
- Errores tipados para que puedas usar
catch (err) { if (err instanceof ElidoRateLimitError) { … } }en lugar de analizar JSON en bloques catch. - Iteradores de paginación para que los endpoints de listado expongan iteradores asíncronos o generadores en lugar de requerir el manejo manual de cursores.
El SDK de Go expone además el cliente HTTP subyacente para instrumentación, útil si quieres conectarlo a tu configuración de rastreo existente. La página de características de API + SDKs del repositorio cubre toda la superficie; la referencia de la API se publica en /docs/api-reference.
Acceso a analíticas#
Los endpoints de analíticas son de solo lectura y residen bajo /v1/workspaces/{id}/analytics/. Las consultas más comunes:
GET .../links/{id}/clicks?from=…&to=…: eventos de clics brutos con paginación. Útil para pipelines de exportación.GET .../timeseries?from=…&to=…&bucket=day: recuentos de clics agrupados por periodos de tiempo.GET .../breakdown/country?from=…&to=…: desglose geográfico.GET .../breakdown/referrer?from=…&to=…: desglose por referente.
El feed de eventos de clics brutos es el más grande. Un espacio de trabajo con 10 millones de clics al mes produce unos 600 MB de JSON al mes de datos de eventos brutos. Para exportaciones a esta escala, la guía de exportación a ClickHouse cubre el mecanismo de exportación masiva que evita la envoltura JSON y transmite directamente desde el almacén de analíticas.
Webhooks para eventos de clics#
Los webhooks son lo opuesto al polling: en lugar de que tú preguntes a la API por nuevos clics, la API los entrega a tu endpoint. Configura en /settings/webhooks:
await elido.webhooks.create({
url: "https://your-app.example/webhooks/elido",
events: ["link.click", "link.created", "link.expired"],
secret: process.env.WEBHOOK_SIGNING_SECRET,
});
Cada entrega incluye un encabezado Elido-Signature que contiene un HMAC-SHA256 del cuerpo de la solicitud con tu secreto compartido. Verifica la firma antes de procesar; sin ella, cualquier llamador puede enviar datos a tu endpoint de webhook e suplantar a Elido.
La semántica de entrega es al-menos-una-vez (at-least-once) con retroceso exponencial hasta una retención máxima de 72 horas. Para conocer la forma detallada y el comportamiento de reintento, el post de webhooks vs polling compara los dos patrones de integración.
Un ejemplo práctico: automatización de campañas#
La integración que motiva la mayor parte de la adopción de la API se ve así: Tu automatización de marketing crea una campaña en Customer.io o HubSpot. Se dispara un hook cuando se publica la campaña. Tu controlador crea el enlace corto, lo adjunta al registro de la campaña y lo envía de vuelta a la herramienta de gestión de campañas para sustituirlo en la plantilla de correo electrónico.
En TypeScript:
import { Elido } from "@elido/sdk";
const elido = new Elido({ token: process.env.ELIDO_TOKEN! });
export async function onCampaignPublished(campaign: Campaign) {
const link = await elido.links.create(
{
destinationUrl: campaign.destinationUrl,
tags: ["campaign", `campaign:${campaign.id}`, campaign.channel],
utm: {
source: campaign.channel,
medium: "email",
campaign: campaign.slug,
},
metadata: { campaign_id: campaign.id, batch: campaign.batchId },
},
{
idempotencyKey: `campaign-${campaign.id}-link`,
},
);
await campaignStore.update(campaign.id, { shortUrl: link.shortUrl });
return link;
}
La clave de idempotencia se deriva del ID de la campaña. Si el hook de campaña publicada se dispara dos veces (sucede: las entregas de webhooks son al-menos-una-vez), la segunda llamada devuelve el mismo enlace sin crear un duplicado. El campo metadata contiene tus propias claves de unión para que puedas correlacionar los eventos de clics de Elido con la campaña sin tener que analizar etiquetas.
Para la atribución de campañas de extremo a extremo con plantillas UTM y reenvío de conversiones, el post sobre rastreo UTM recorre todo el pipeline.
Lo que aún no está en la API#
Dos cosas que se preguntan con frecuencia y que actualmente no están disponibles:
- Un GET de analíticas de un solo enlace que devuelva todos los desgloses en una sola llamada. El modelo actual requiere llamadas separadas para clics, país, referente, dispositivo y series temporales. La agregación está en la hoja de ruta; por ahora, los SDK paralelizan las solicitudes con un único método de ayuda.
- Reintento de webhooks desde la API. El panel de control expone el historial de entrega de webhooks y admite el reintento; la API aún no. Esto también está en la hoja de ruta.
Si una característica está en la especificación OpenAPI, está soportada. Si está en este post pero no en la especificación, trátala como planificada en lugar de garantizada.
Lecturas relacionadas#
- Explicación de los enlaces inteligentes: la pieza fundamental para el cluster de funciones; cubre cómo el motor de redirección resuelve un enlace en el edge.
- Webhooks vs polling para el rastreo de clics: cuándo usar cada patrón de integración.
- Rastreo de conversiones del lado del servidor a través de enlaces cortos: extendiendo la API al flujo de reenvío de conversiones.
- Importación masiva de campañas desde Google Sheets: un ejemplo práctico del endpoint masivo.
- Recorrido operativo: la guía del servidor MCP para conectar la superficie de la API de Elido a Claude, Cursor y otros clientes compatibles con MCP.
- Superficie del producto:
/features/api-sdksy/solutions/developers.