Acortar una URL en PHP es un único POST HTTP. Envía el enlace largo a la API de un acortador con la extensión curl, pasa tu clave de API como token Bearer y extrae el enlace corto de la respuesta con json_decode. No hace falta ningún paquete de Composer, aunque Guzzle y el cliente HTTP de Laravel reducen la misma llamada a tres líneas.
La mayoría de los tutoriales de PHP sobre este tema son piezas de museo. Configuran urlshortener/v1, que Google retiró después de poner fin a la creación de enlaces nuevos en 2019, mientras que los enlaces goo.gl inactivos dejaron de funcionar el 25 de agosto de 2025, o utilizan el endpoint bit.ly v3, que desapareció hace años. El código siguiente apunta a una API actual.
Esta es la versión para PHP de la guía general sobre cómo acortar una URL. Si todavía no has elegido un servicio, el resumen de la API gratuita de acortamiento de URL explica la forma de la solicitud, el modelo de autenticación y los límites que damos por supuestos aquí. Los nombres de los campos son los de Elido; la estructura se puede adaptar.
La forma más rápida: un POST de curl
Guarda la clave en el entorno, codifica tú mismo el cuerpo y lee el código de estado antes de confiar en la carga útil:
<?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?utm_source=newsletter',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($body === false || $status >= 400) {
throw new RuntimeException("shorten failed: HTTP {$status}");
}
$link = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
echo $link['short_url']; // https://s.elido.me/ab12cd
Hay dos líneas que existen porque todo el mundo se encuentra con esos errores alguna vez. CURLOPT_RETURNTRANSFER tiene false como valor predeterminado, así que sin ella curl imprime el JSON directamente en la salida y te entrega true; esa es la verdadera explicación de la mayoría de las preguntas del tipo «mi respuesta está vacía». Además, CURLOPT_POSTFIELDS cambia a la codificación de formulario multipart en cuanto le pasas un array, por lo que json_encode es lo que convierte la solicitud en una solicitud JSON.
CURLINFO_RESPONSE_CODE también importa. curl no tiene ningún problema en devolver un cuerpo lleno de {"error": "unauthorized"} sin quejarse.
La misma llamada en Guzzle o Laravel
Si el proyecto ya incluye Guzzle, el código repetitivo se reduce:
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://api.elido.app',
'timeout' => 10,
'headers' => ['Authorization' => 'Bearer ' . getenv('ELIDO_API_KEY')],
]);
$response = $client->post('/v1/links', [
'json' => ['destination_url' => $destination],
'headers' => ['Idempotency-Key' => hash('sha256', $destination)],
]);
$short = json_decode((string) $response->getBody(), true)['short_url'];
Guzzle lanza ClientException ante respuestas 4xx y ServerException ante respuestas 5xx de forma predeterminada, lo contrario del silencio de curl y de lo que hace fetch en JavaScript. Captúralas, o configura http_errors como false y comprueba el estado tú mismo.
En Laravel, el cliente HTTP envuelve Guzzle y te ofrece reintentos sin coste adicional:
$short = Http::withToken(config('services.elido.key'))
->timeout(10)
->retry(3, 200, throw: false)
->post('https://api.elido.app/v1/links', ['destination_url' => $destination])
->json('short_url');
Ese retry(3, 200) son tres intentos con una pausa de 200 ms. Basta para un fallo transitorio, pero no para un límite de solicitudes; esa es la siguiente sección.
Acortar enlaces dentro de WordPress
WordPress no usa curl directamente. Tiene wp_remote_post, que elige un transporte por ti y funciona en alojamientos donde curl está desactivado. Conéctalo a la publicación y guarda el resultado como metadatos de la entrada para que las plantillas y los feeds puedan leerlo:
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)) {
error_log('shorten failed: ' . $response->get_error_message());
return;
}
$link = json_decode(wp_remote_retrieve_body($response), true);
update_post_meta($post_id, '_elido_short_url', $link['short_url']);
}, 10, 1);
ELIDO_API_KEY debe estar en wp-config.php, no en un archivo de plugin que termine en un repositorio público ni en una fila de opciones que cualquier administrador pueda leer. Fíjate en la clave de idempotencia: post-123 es estable, así que si el hook se ejecuta dos veces al volver a publicar, la segunda llamada devuelve el enlace que ya existe en lugar de crear otro.
Si prefieres no mantener el hook, la guía del acortador de WordPress compara esta opción con la del plugin y las alternativas sin código.
¿Quieres ejecutar estos fragmentos tal cual? Crea una clave en el plan gratuito, expórtala como ELIDO_API_KEY y el bloque de curl anterior funcionará sin cambios.
Reintentos, límites de solicitudes e idempotencia
El PHP desatendido, ya sea un trabajo cron, un trabajador de cola o un importador masivo, debe sobrevivir a un 429 y a un 5xx transitorio. También debe sobrevivir al caso incómodo en el que el POST llega a la API, pero la respuesta se pierde durante el regreso. Tu código ve un tiempo de espera agotado, vuelve a intentarlo y crea un segundo enlace corto para el mismo destino, a menos que una clave de idempotencia lo impida.
function shorten(string $destination, int $retries = 3): string
{
$key = hash('sha256', $destination); // stable across retries and re-runs
for ($attempt = 0; $attempt < $retries; $attempt++) {
[$body, $status, $retryAfter] = post_link($destination, $key);
if ($status === 429) {
sleep(max(1, (int) $retryAfter));
continue;
}
if ($status >= 500) {
sleep(2 ** $attempt); // exponential backoff
continue;
}
if ($status >= 400) {
throw new RuntimeException("shorten failed: HTTP {$status} {$body}");
}
return json_decode($body, true, 512, JSON_THROW_ON_ERROR)['short_url'];
}
throw new RuntimeException("shorten failed after {$retries} attempts");
}
Un 401 o un 403 nunca se arreglarán por sí solos, así que se lanza una excepción en el primer intento en lugar de esperar tres veces. Un 429 espera tanto como indique el encabezado Retry-After. Solo los 5xx reciben el retroceso progresivo que duplica la espera. El análisis detallado de los límites de solicitudes y la idempotencia explica todo el razonamiento, incluido por qué un hash del destino suele ser una clave mejor que un UUID aleatorio cuando se puede volver a ejecutar el mismo lote.
Una trampa específica de PHP: sleep() dentro de una solicitud web mantiene ocupado un trabajador de php-fpm durante todo el tiempo. Los reintentos pertenecen a scripts de CLI, trabajos de cola o WP-Cron, no a la solicitud que renderiza una página.
Acortar en masa sin esperas en serie
Un bucle foreach que acorta 500 URL hace 500 viajes de ida y vuelta consecutivos. A 120 ms cada uno, es un minuto de espera pura. PHP no tiene un entorno de ejecución asíncrono, pero sí tiene HTTP simultáneo, y Pool de Guzzle mantiene un número fijo de solicitudes en curso:
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;
$requests = static function (array $urls) {
foreach ($urls as $url) {
yield new Request('POST', '/v1/links', [
'Content-Type' => 'application/json',
'Idempotency-Key' => hash('sha256', $url),
], json_encode(['destination_url' => $url]));
}
};
$short = [];
$pool = new Pool($client, $requests($urls), [
'concurrency' => 8,
'fulfilled' => function ($response, $i) use (&$short, $urls) {
$short[$urls[$i]] = json_decode((string) $response->getBody(), true)['short_url'];
},
'rejected' => function ($reason, $i) use (&$short, $urls) {
$short[$urls[$i]] = 'ERROR: ' . $reason->getMessage();
},
]);
$pool->promise()->wait();
Usar la URL original como clave de los resultados hace visible cualquier fallo parcial y permite volver a ejecutarlo. Sin Composer, curl_multi_init hace el mismo trabajo con más gestión: añade manejadores, itera con curl_multi_exec y recopila cada respuesta. Empieza con una concurrencia de aproximadamente ocho y deja que los encabezados de respuesta te indiquen si debes reducirla.
Qué opción elegir
Adapta la herramienta al alojamiento. Alojamiento compartido sin Composer: el bloque de curl puro. Una aplicación Symfony o Laravel: Guzzle o el cliente HTTP, con los reintentos configurados una sola vez en el nivel del cliente. WordPress: wp_remote_post en un hook de publicación. Una importación nocturna de miles de enlaces: el grupo, con una clave de idempotencia estable para que volver a ejecutarlo no tenga coste.
Elijas la que elijas, cuatro cosas no cambian: la clave procede del entorno, curl recibe un tiempo de espera explícito, el código de estado se lee antes que el cuerpo y todo lo que pueda ejecutarse dos veces lleva una clave de idempotencia. Los equipos que superan el primer script suelen querer la API y los SDK o lo que la plataforma ofrece a los desarrolladores: clics, etiquetas, caducidad, webhooks para eventos de enlaces en lugar de sondeo.
Lee la serie central
Este artículo forma parte del clúster de ingeniería. Empieza por la guía de la API gratuita de acortamiento de URL para conocer la estructura del endpoint y la autenticación; después, consulta el artículo sobre límites de solicitudes e idempotencia para comportarte correctamente bajo carga. La referencia actual es la documentación de la API, y si heredaste enlaces goo.gl, la alternativa a Google URL Shortener explica adónde van ahora.
Relacionado en el blog
- Cómo acortar una URL en JavaScript con fetch y Node
- Cómo acortar una URL en Python con la biblioteca requests
- Cómo acortar una URL en Go con net/http
- Acortador de URL para WordPress: 4 formas de acortar y rastrear enlaces
- Cómo acortar una URL en Ruby con Net::HTTP y Faraday
- Cómo acortar una URL en C# con HttpClient y .NET
Preguntas frecuentes
¿Cómo acorto una URL en PHP?
Envía la URL larga mediante POST a la API de un acortador con curl, incluye tu clave de API como encabezado Bearer y el destino en un cuerpo JSON, luego usa json_decode para leer el campo short_url de la respuesta. Son aproximadamente quince líneas con curl_setopt_array, o tres con Guzzle o el cliente HTTP de Laravel si el proyecto ya usa uno.
¿Cómo acorto una URL en PHP sin una biblioteca externa?
Usa la extensión curl que viene con PHP. curl_init, curl_setopt_array, curl_exec y json_decode cubren todo el flujo sin una dependencia de Composer, algo importante en un alojamiento compartido donde no puedes ejecutar Composer. Configura CURLOPT_RETURNTRANSFER como true o curl_exec imprimirá la respuesta en lugar de devolverla.
¿La API de Google URL Shortener sigue funcionando en PHP?
No. Google dejó de aceptar enlaces nuevos mediante la API de URL Shortener en 2019 y retiró el servicio; además, los enlaces goo.gl que habían quedado inactivos dejaron de resolver el 25 de agosto de 2025. Cualquier tutorial de PHP basado en urlshortener/v1 es código obsoleto, y los ejemplos de bit.ly v3 de la misma época también desaparecieron.
¿Puedo acortar una URL dentro de WordPress con PHP?
Sí. Llama a wp_remote_post desde un hook que se ejecute al publicar y guarda el enlace corto devuelto con update_post_meta para que el resto del tema pueda leerlo. Mantén la clave de API en wp-config.php, no en un archivo del plugin ni en la base de datos, y comprueba is_wp_error en la respuesta porque WordPress devuelve un objeto WP_Error en lugar de lanzar una excepción.
¿Cómo acorto muchas URL a la vez en PHP?
Envía las solicitudes simultáneamente con un grupo limitado en lugar de usar un bucle foreach que espera cada respuesta. Pool de Guzzle con una concurrencia de aproximadamente ocho es la ruta más corta, curl_multi hace lo mismo sin Composer, y una Idempotency-Key derivada de cada URL evita que una nueva ejecución cree enlaces duplicados.
¿Por qué mi solicitud de PHP con curl a un acortador de URL devuelve 401 o una respuesta vacía?
Un 401 significa que falta el encabezado Authorization o está mal formado: debe leerse exactamente 'Authorization: Bearer YOUR_KEY' como una sola cadena dentro del array CURLOPT_HTTPHEADER. Un valor de retorno vacío suele significar que nunca se configuró CURLOPT_RETURNTRANSFER, por lo que el cuerpo pasó directamente a la salida y curl_exec devolvió true en su lugar.
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