7 min de leituraEngenharia

How to Shorten a URL in PHP With curl, Guzzle, or WordPress

Shorten a URL in PHP with a short curl block, the same call in Guzzle or Laravel, a WordPress version with wp_remote_post, plus retries and bulk shortening.

Ana Kowalska
Marketing solutions engineering
How to shorten a URL in PHP: a curl POST sending a destination URL to a shortener API and decoding the short link from the JSON response, alongside the Guzzle and WordPress paths

Shortening a URL in PHP is one HTTP POST. Send the long link to a shortener's API with the curl extension, pass your API key as a Bearer token, and json_decode the short link out of the response. No Composer package is required, though Guzzle and the Laravel HTTP client shorten the same call to three lines.

Most PHP tutorials on this are museum pieces. They wire up urlshortener/v1, which Google retired after ending new link creation in 2019, with inactive goo.gl links stopping on 25 August 2025, or they use the bit.ly v3 endpoint that went away years ago. The code below targets an API that is current.

This is the PHP version of the general how-to-shorten-a-url walkthrough. If you have not chosen a service, the free URL shortener API overview covers the request shape, the auth model, and the limits assumed here. Field names are Elido's; the shape ports.

The Fastest Way: One curl POST

Put the key in the environment, encode the body yourself, and read the status code before you trust the payload:

<?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

Two lines in there exist because of bugs everyone hits once. CURLOPT_RETURNTRANSFER defaults to false, so without it curl prints the JSON straight to output and hands you true, which is the real story behind most "my response is empty" questions. And CURLOPT_POSTFIELDS switches to multipart form encoding the moment you pass it an array, so the json_encode is what actually makes it a JSON request.

CURLINFO_RESPONSE_CODE matters just as much. curl is happy to return a body full of {"error": "unauthorized"} with no complaint at all.

A PHP curl POST sending a destination URL and Bearer token to the shortener links endpoint, the API returning HTTP 201 with a short_url that json_decode reads, and a retry loop backing off on 429 and 5xx responses

The Same Call in Guzzle or Laravel

If the project already pulls in Guzzle, the boilerplate collapses:

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 throws ClientException on 4xx and ServerException on 5xx by default, which is the opposite of curl's silence and the opposite of JavaScript's fetch. Catch them, or set http_errors to false and check the status yourself.

On Laravel, the HTTP client wraps Guzzle and gives you retries for free:

$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');

That retry(3, 200) is three attempts with a 200 ms pause. It is enough for a transient blip and not enough for a rate limit, which is the next section.

WordPress does not use curl directly. It has wp_remote_post, which picks a transport for you and works on hosts where curl is disabled. Hook it to publish, then store the result as post meta so templates and feeds can read it:

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 belongs in wp-config.php, not in a plugin file that ends up in a public repository and not in an options row that every admin can read. Note the idempotency key: post-123 is stable, so if the hook fires twice on a re-publish, the second call returns the link that already exists instead of minting a second one.

If you would rather not maintain the hook, the WordPress shortener guide compares this with the plugin and no-code routes.

Want to run these snippets as written? Create a key on the free plan, export it as ELIDO_API_KEY, and the curl block above works unchanged.

Retries, Rate Limits, and Idempotency

Unattended PHP - a cron job, a queue worker, a bulk importer - needs to survive a 429 and a transient 5xx. It also needs to survive the awkward case where the POST reaches the API but the response is lost on the way back. Your code sees a timeout, retries, and creates a second short link for the same destination unless an idempotency key stops it.

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");
}

A 401 or 403 will never repair itself, so it throws on the first attempt rather than sleeping through three. A 429 waits for as long as the Retry-After header asks. Only 5xx gets the doubling backoff. The rate limits and idempotency deep-dive has the full reasoning, including why a hash of the destination is usually a better key than a random UUID when the same batch might be re-run.

One PHP-specific trap: sleep() inside a web request holds a php-fpm worker the whole time. Retries belong in CLI scripts, queue jobs, or WP-Cron - not in the request that renders a page.

Bulk Shortening Without Serial Waits

A foreach loop that shortens 500 URLs makes 500 round trips back to back. At 120 ms each that is a minute of pure waiting. PHP has no async runtime, but it does have concurrent HTTP, and Guzzle's Pool keeps a fixed number of requests in flight:

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();
Three PHP paths to the same shortener endpoint: plain curl on shared hosting, Guzzle or Laravel in an application, and wp_remote_post inside WordPress, all reading the API key from the environment

Keying results by the original URL keeps a partial failure visible and re-runnable. Without Composer, curl_multi_init does the same job with more bookkeeping: add handles, loop on curl_multi_exec, collect each response. Start concurrency around eight and let the response headers tell you whether to come down.

Which Approach to Reach For

Match the tool to the host. Shared hosting with no Composer: the plain curl block. A Symfony or Laravel app: Guzzle or the HTTP client, with retries configured once at the client level. WordPress: wp_remote_post on a publish hook. A nightly import of thousands of links: the pool, with a stable idempotency key so a re-run is free.

Whichever you pick, four things stay the same: the key comes from the environment, curl gets an explicit timeout, the status code is read before the body, and anything that might run twice carries an idempotency key. Teams that get past the first script usually want the API and SDKs or what else the platform exposes to developers - clicks, tags, expiry, webhooks for link events instead of polling.

Read the Cornerstone Series

This sits in the engineering cluster. Start with the free URL shortener API guide for the endpoint shape and auth, then the rate limits and idempotency piece for behaving well under load. The live reference is the API docs, and if you inherited goo.gl links, the Google URL Shortener alternative covers where they go now.

Perguntas frequentes

How do I shorten a URL in PHP?

POST the long URL to a shortener's API with curl, sending your API key as a Bearer header and the destination in a JSON body, then json_decode the response and read the short_url field. It is roughly fifteen lines with curl_setopt_array, or three with Guzzle or the Laravel HTTP client if the project already has one.

How do I shorten a URL in PHP without an external library?

Use the curl extension that ships with PHP. curl_init, curl_setopt_array, curl_exec and json_decode cover the whole flow with no Composer dependency, which matters on shared hosting where you cannot run Composer. Set CURLOPT_RETURNTRANSFER to true or curl_exec prints the response instead of returning it.

Does the Google URL Shortener API still work in PHP?

No. Google stopped accepting new links through the URL Shortener API in 2019 and retired the service, and goo.gl links that had gone inactive stopped resolving on 25 August 2025. Any PHP tutorial built on urlshortener/v1 is dead code, and the bit.ly v3 examples of the same era are gone too.

Can I shorten a URL inside WordPress with PHP?

Yes. Call wp_remote_post from a hook that fires on publish, then store the returned short link with update_post_meta so the rest of the theme can read it. Keep the API key in wp-config.php rather than in a plugin file or the database, and check is_wp_error on the response because WordPress returns a WP_Error object rather than throwing.

How do I shorten many URLs at once in PHP?

Send the requests concurrently with a bounded pool rather than a foreach loop that waits for each response. Guzzle's Pool with a concurrency of about eight is the shortest route, curl_multi does the same without Composer, and an Idempotency-Key derived from each URL keeps a re-run from creating duplicate links.

Why does my PHP curl request to a URL shortener return 401 or an empty response?

A 401 means the Authorization header is missing or malformed - it must read exactly 'Authorization: Bearer YOUR_KEY' as a single string in the CURLOPT_HTTPHEADER array. An empty return value usually means CURLOPT_RETURNTRANSFER was never set, so the body went straight to output and curl_exec returned true instead.

Experimente Elido

Cole uma URL, obtenha um link curto

Sem cadastro. O link vive 30 dias. Cadastre-se para mantê-lo para sempre.

Grátis, sem necessidade de registo · 2 por dia

Experimente o Elido

Encurtador de URL hospedado na UE: domínios personalizados, análises profundas e API aberta. Plano gratuito - sem cartão de crédito.

Tags
how to shorten a url in php
php url shortener
shorten url php curl
url shortener api php
wordpress shorten url php
bulk shorten urls php

Continuar lendo