14 min de lecturaFunciones

API de acortador de URL: una guía rápida de 30 minutos en cinco lenguajes

De cero a una automatización de enlaces cortos funcionando en TypeScript, Python, Go, Ruby y PHP: autenticación, idempotencia, gestión de errores y los detalles que importan en producción.

Marius Voß
DevRel · edge infra
Diagrama de guía rápida en cinco lenguajes con paneles de código para TypeScript, Python, Go, Ruby y PHP, todos apuntando a un endpoint central de la API de Elido

Una API de acortador de URL es una de las integraciones más pequeñas en el backlog típico de un equipo de ingeniería. Tres endpoints, una cabecera 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 reintentos crea enlaces duplicados, el dashboard se llena de variantes /foo-1, /foo-2, /foo-3 del mismo destino, y alguien abre un ticket.

Este artículo recorre la integración real. Autenticación, la primera llamada, los cuatro endpoints que cubren la mayoría de los casos de uso, idempotencia, gestión de errores, límites de tasa y los detalles de producción que la guía rápida de cinco minutos se salta. Ejemplos de código en TypeScript, Python, Go, Ruby y PHP: los tres primeros a través de los SDKs oficiales (@elido/sdk, elido-python, github.com/elido/elido-go), los dos últimos a través de clientes HTTP simples.

Requisitos previos

Inicia sesión en el dashboard, navega a /dashboard/api-keys y crea una clave de API (empieza por elido_). Los tokens están limitados por workspace: un token emitido en el workspace A no puede crear enlaces en el workspace B. Los tokens de usuario-máquina (para sistemas de CI, herramientas internas, integración máquina a máquina) se crean en /dashboard/machine-users y rotan de forma independiente a las claves personales. Ambos tipos llevan un rol de workspace preestablecido (viewer, editor o admin) en lugar de permisos por endpoint, así que dale editor a un job de CI si solo crea enlaces. La guía sobre permisos de las claves de API explica a qué puede acceder cada rol, incluido por qué los cambios en webhooks requieren admin.

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 es para crearlos, modificarlos y leerlos.

La especificación OpenAPI se publica en https://elido.app/openapi.json y cumple con OpenAPI 3.1. Los SDKs oficiales se generan a partir de esa especificación y se vuelven a publicar con cada versión 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 canónico del enlace, el ID del workspace y la marca de tiempo de creación. El slug (abc123 en el ejemplo anterior) lo genera el servidor a menos que pases slug en la solicitud. El alfabeto del slug es base62 ([0-9A-Za-z]); la longitud por defecto es de seis caracteres.

Los cuatro endpoints que realmente vas a usar

La API tiene más de cuatro endpoints, pero la mayoría de las integraciones se quedan dentro de este conjunto.

Diagrama en estrella de los cuatro endpoints principales de enlaces alrededor del recurso /v1/links: POST crear, GET leer, PATCH actualizar y DELETE eliminar, cada uno con su detalle clave.

Crear un enlace

POST /v1/links acepta la URL de destino más campos opcionales:

  • slug: un slug que eliges (debe ser único en el dominio).
  • domain_id: para enlaces de dominio personalizado; en /v1/links se usa el dominio corto predeterminado de tu plan si se omite. La ruta con ámbito de workspace /v1/workspaces/{workspace_id}/links lo requiere.
  • title: una etiqueta que se muestra en el dashboard.
  • tags: un array de cadenas libres para organización.
  • expires_at: marca de tiempo RFC 3339 tras la cual el enlace devuelve 410 Gone.
  • redirect_status: 301, 302 (el predeterminado) o 307.
  • password: todavía no se acepta al crear; establécela con un PATCH justo después, y la redirección mostrará una página con contraseña antes de reenviar.
  • utm y metadata: previstas. Hoy, incluye los parámetros UTM directamente en destination_url y guarda tus propias claves de unión en tags.

El slug personalizado es el campo que muerde a los equipos en producción. Si pasas un slug ya usado por otro enlace en el mismo dominio, la API devuelve 409 Conflict. El manejador de reintentos ingenuo que añade un contador (my-slug-1, my-slug-2) produce el problema de enlaces duplicados descrito en la introducción. El comportamiento de reintento correcto se describe en la sección de idempotencia más abajo.

Leer un enlace

GET /v1/links/{id} devuelve el registro completo del enlace, incluyendo short_url y toda la configuración. Los conteos de clics no están en el registro del enlace; proceden de los endpoints de analítica que aparecen más abajo. El ID del enlace es el identificador canónico: los slugs pueden cambiar, los IDs no.

GET /v1/links?host=…&tags=…&limit=… lista los enlaces del workspace con filtros. La paginación se basa en cursor; next_cursor en la respuesta es opaco y se pasa de vuelta 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 rotar campañas sin reimprimir códigos QR), cambiar etiquetas, extender expires_at. El slug se cambia mediante el mismo PATCH, enviando un nuevo slug. El slug antiguo deja de resolverse de inmediato; un endpoint de renombrado específico que mantenga una redirección 301 desde el slug antiguo durante un periodo de retención está previsto, pero aún no existe.

Eliminar un enlace

DELETE /v1/links/{id} realiza un borrado lógico y devuelve 204 No Content. El enlace deja de redirigir y desaparece de las llamadas de listado y lectura. Una vista de papelera con un endpoint de restauración y una ventana de 90 días antes de la eliminación definitiva están previstos; hoy no existe ninguna llamada de API que permita recuperar un enlace eliminado.

Claves de idempotencia

Cada solicitud mutante (POST, PATCH, DELETE) acepta una cabecera Idempotency-Key. El valor de la cabecera 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, indexados por (workspace_id, idempotency_key), y devuelve la respuesta almacenada si se presenta la misma clave otra vez.

Los SDKs oficiales generan claves de idempotencia automáticamente cuando no se proporcionan. Puedes anularlas:

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 reintentos. Si tu job crea un enlace como parte del procesamiento de un pedido que viene antes en la cadena, genera la clave de idempotencia a partir del ID del pedido. Un reintento del mismo job ve la misma clave, encuentra la caché de idempotencia, y devuelve el enlace creado originalmente en vez de producir uno segundo.

Pipeline donde un hook de campaña de entrega al menos una vez dispara dos llamadas de creación con la misma clave de idempotencia; la caché de 24 horas deduplica la segunda para que se cree exactamente un enlace.

El detalle clave: la caché de idempotencia vive durante 24 horas, no para siempre. Un reintento al tercer día de un job atascado creará un enlace nuevo. Si la integración se ejecuta en lotes de varios días, guarda el ID del enlace devuelto por la primera creación exitosa y búscalo antes de volver a emitir la solicitud.

Un segundo detalle: la idempotencia es por workspace. La misma clave en dos workspaces crea dos enlaces. Esta es la semántica correcta para una API multi-workspace, pero puede sorprender a los equipos que asumen que la clave es globalmente única.

Gestión 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: fallo de validación del payload. El campo message enumera 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 rol del token no permite la acción (una clave viewer no puede crear enlaces). Revisa el rol de la clave en /dashboard/api-keys.
  • 404 not_found: el recurso no existe o el token no tiene acceso a él (devolvemos 404 en vez de 403 para evitar filtrar la existencia del recurso a llamadores no autorizados).
  • 409 conflict: slug ya en uso, o edición simultánea detectada (PATCH sobre una versión desactualizada). Vuelve a obtener el recurso y reintenta.
  • 429 rate_limit_exceeded: espera según el valor de retry_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 de infraestructura transitorios. Espera y reintenta.

Los SDKs oficiales implementan retroceso exponencial con jitter para 429, 500, 502, 503 y 504. No reintentan 400, 401, 403, 404 ni 409: esos son errores de programación o conflictos de lógica de negocio, no fallos transitorios. Los clientes HTTP personalizados deberían seguir el mismo patrón; reintentar un 400 con el mismo payload no producirá un resultado distinto.

División de decisión que clasifica los códigos de estado de la API en una columna de reintento con retroceso (429, 500, 502, 503, 504) y una columna de no reintentar (400, 401, 403, 404, 409) para errores de programación y de conflicto.

El request_id en el cuerpo del error es el campo que hay que incluir en los tickets de soporte. Podemos rastrear cualquier solicitud a partir de ese ID a través del registro de auditoría, el log 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 workspace 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 cabeceras de respuesta en cada respuesta de la API:

  • X-RateLimit-Limit: el límite por segundo actual.
  • X-RateLimit-Remaining: solicitudes restantes en el segundo actual.
  • X-RateLimit-Reset: marca de tiempo Unix en la que se reinicia el cubo.

El límite de 100/s es una implementación de token bucket con una capacidad de ráfaga de 200, lo que significa que puedes emitir 200 solicitudes de golpe si el cubo está lleno, y luego asentarte en la tasa sostenida de 100/s. La mayoría de los jobs de creación de enlaces cortos caben cómodamente en la ráfaga; las integraciones intensivas en analítica que paginan a través de eventos de clic históricos se benefician del margen del nivel Pro.

Para operaciones masivas, el endpoint POST /v1/links/bulk acepta hasta 100 enlaces por solicitud y cuenta como una sola unidad de límite de tasa. Este es el endpoint correcto para cualquier job que cree más de un centenar de enlaces a la vez. Para el tratamiento más profundo sobre cómo marcar el ritmo contra el token bucket, qué códigos de estado reintentar, y cómo las claves de idempotencia evitan que los reintentos dupliquen enlaces, consulta límites de tasa, reintentos e idempotencia en producción.

Lo que hacen los SDKs que el HTTP simple no hace

Los SDKs oficiales incluyen cuatro cosas que se amortizan rápido:

  • 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 hacer catch (err) { if (err instanceof ElidoRateLimitError) { … } } en vez de analizar JSON en los bloques catch.
  • Iteradores de paginación para que los endpoints de listado expongan iteradores asíncronos o generadores en vez de exigir manejo manual del cursor.

El SDK de Go además expone el cliente HTTP subyacente para instrumentación, útil si quieres conectarlo a tu configuración de trazado existente. La página de la función API + SDKs del repositorio cubre la superficie completa; la referencia de la API se publica en /docs/api-reference.

Acceso a 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 enumera todos los informes, sus parámetros y la forma de sus respuestas. Las consultas más comunes:

  • GET .../clicks/recent?from=…&to=…: clics individuales, del más reciente al más antiguo, paginados con next_cursor. Útil para pipelines de exportación.
  • GET .../timeseries?from=…&to=…&interval=day: conteos de clics agrupados para un rango de tiempo; interval puede ser hour o day, y tz establece la zona horaria de las agrupaciones.
  • GET .../breakdown/country?from=…&to=…: desglose geográfico.
  • GET .../breakdown/referrer?from=…&to=…: desglose por referrer.

Los demás informes son summary, links/top, los desgloses restantes (host, device, browser, destination) y las listas principales (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 limitar cualquier informe a un solo enlace, y limit para dimensionar los desgloses y las listas principales.

El feed de eventos de clic sin procesar es el más grande. Un workspace con 10 millones de clics al mes produce unos 600 MB de datos de eventos en JSON al mes. Para exportaciones a esta escala, la guía de exportación de analítica cubre el mecanismo de exportación masiva que evita el envoltorio JSON y transmite directamente desde el almacén de analítica.

Webhooks para eventos de enlaces

Los webhooks son el inverso del sondeo: en vez de que tú le preguntes a la API qué cambió, la API entrega los eventos de enlaces y dominios a tu endpoint. Configúralo en /dashboard/webhooks:

await elido.webhooks.create({
  url: "https://your-app.example/webhooks/elido",
  events: ["link.created", "link.updated", "link.expired"],
  secret: process.env.WEBHOOK_SIGNING_SECRET,
});

Un evento click.created por cada clic está en la hoja de ruta pero todavía no disponible, así que hoy los datos de clics vienen de los endpoints de analítica. Cada entrega incluye una cabecera X-Elido-Signature (también enviada como X-Webhook-Signature) con el valor v1=<hex>: un HMAC-SHA256, con clave de tu secreto de endpoint, sobre el valor X-Webhook-Timestamp, un punto y el cuerpo de la solicitud sin procesar. Verifica la firma antes de procesar; sin ella, cualquier llamador puede enviar a tu endpoint de webhook y suplantar a Elido.

La semántica de entrega es "al menos una vez": una entrega fallida se reintenta con un retroceso de minutos, con tres intentos en total por defecto. Para la forma detallada y el comportamiento de reintento, el artículo webhooks frente a sondeo compara los dos patrones de integración.

Un ejemplo trabajado: automatización de campañas

La integración que motiva la mayoría de las adopciones de la API se ve así. Tu automatización de marketing crea una campaña en Customer.io o HubSpot. Un hook se dispara cuando se publica la campaña. Tu manejador crea el enlace corto, lo adjunta al registro de la campaña, y lo devuelve a la herramienta de gestión de campañas para sustituirlo en la plantilla de email.

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}`,
        `batch:${campaign.batchId}`,
        campaign.channel,
      ],
    },
    {
      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 (ocurre: las entregas de webhooks son al menos una vez), la segunda llamada devuelve el mismo enlace sin crear un duplicado. Las etiquetas campaign: y batch: contienen tus propias claves de unión para que puedas correlacionar los eventos de clic de Elido con la campaña; está previsto añadir un campo metadata específico para esto. Los parámetros UTM deben estar en campaign.destinationUrl mientras el campo utm no esté disponible.

Para la atribución de campaña de principio a fin con plantillas UTM y reenvío de conversiones, la referencia de seguimiento de UTM recorre el pipeline completo.

Lo que todavía no está en la API

Dos cosas que se preguntan con frecuencia, actualmente no disponibles:

  • Un único GET de analítica por enlace que devuelva todos los desgloses en una sola llamada. El modelo actual requiere llamadas separadas para clics, país, referrer, dispositivo y serie temporal. La agregación está en la hoja de ruta; por ahora, ejecuta las solicitudes en paralelo desde tu propio código.
  • Reenvío (replay) de webhooks desde la API. El dashboard expone el historial de entregas de webhooks y admite el reenvío; la API todavía no. Esto también está en la hoja de ruta.

Si una funcionalidad está en la especificación OpenAPI, está soportada. Si está en este artículo pero no en la especificación, trátala como prevista en vez de garantizada.

Lecturas relacionadas

Prueba Elido

Pega una URL, obtén un enlace corto

Sin registro. El enlace vive 30 días. Crea una cuenta para conservarlo.

Gratis, sin registro · 2 por día

Prueba Elido

Acortador de URL alojado en la UE: dominios personalizados, análisis profundo y API abierta. Plan gratuito - sin tarjeta de crédito.

Etiquetas
url shortener api
bitly api alternative
link shortener api
rest api short link
url shortener sdk
openapi 3.1
idempotency keys

Seguir leyendo