Build links into your product
Everything the dashboard does, your code can do too. JSON in, JSON out, authenticated with a bearer key you generate yourself.
1. Get a key
API access starts on the Starter plan. Sign in, open API in the sidebar and generate a key. It is shown once — store it in your environment, never in your repo.
2. Authenticate
Send the key on every request:
Authorization: Bearer plk_live_YOUR_API_KEY
Accept: application/json
A missing or invalid key returns 401. A key without the
required scope returns 403.
3. Handle the limits
429 — rate limited.
Your plan sets requests per minute. Back off using the
Retry-After header rather than retrying immediately.
402 — out of quota. You have hit your plan's link limit. Delete links or upgrade.
409 — alias taken. Someone already has that custom alias. Pick another or omit it for a random code.
200 vs 201 on create.
If you shorten a URL you have already shortened, we return the existing link with a
200 instead of creating a duplicate. Pass
"reuse_existing": false to force a new code.
Create a short link
curl -X POST https://links.publifying.com/api/v1/links \
-H "Authorization: Bearer plk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"url": "https://example.com/a/very/long/marketing/url?utm_source=newsletter",
"title": "August newsletter",
"tags": ["newsletter", "august"]
}'
const API_KEY = process.env.PUBLIFYING_API_KEY; // plk_live_YOUR_API_KEY
async function shorten(url, options = {}) {
const response = await fetch("https://links.publifying.com/api/v1/links", {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ url, ...options }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message ?? "Failed to shorten link");
}
return response.json(); // { id, code, short_url, destination, clicks, ... }
}
const link = await shorten("https://example.com/very/long/url", {
title: "August newsletter",
});
console.log(link.short_url);
<?php
// Works with any PSR-18 client; this uses plain cURL to stay dependency-free.
function shorten(string $url, array $options = []): array
{
$ch = curl_init(API_ENDPOINT);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('PUBLIFYING_API_KEY'),
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['url' => $url] + $options),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$decoded = json_decode((string) $body, true);
if ($status >= 400) {
throw new RuntimeException($decoded['message'] ?? 'Request failed');
}
return $decoded['data'] ?? $decoded;
}
$link = shorten('https://example.com/very/long/url', ['title' => 'August newsletter']);
echo $link['short_url'];
<?php
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.publifying.key'))
->acceptJson()
->post(config('services.publifying.url') . '/api/v1/links', [
'url' => 'https://example.com/very/long/url',
'title' => 'August newsletter',
'tags' => ['newsletter'],
]);
$response->throw();
$shortUrl = $response->json('data.short_url');
import { useState } from "react";
// Never ship an API key to the browser — call your own backend,
// which holds the key and forwards to https://links.publifying.com.
export function ShortenForm() {
const [url, setUrl] = useState("");
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
async function handleSubmit(event) {
event.preventDefault();
setLoading(true);
setError(null);
try {
const response = await fetch("/api/shorten", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.message);
setResult(payload.data ?? payload);
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://example.com/long-url"
required
/>
<button disabled={loading}>{loading ? "Shortening…" : "Shorten"}</button>
{result && <a href={result.short_url}>{result.short_url}</a>}
{error && <p role="alert">{error}</p>}
</form>
);
}
import os
import requests
API_KEY = os.environ["PUBLIFYING_API_KEY"]
ENDPOINT = "https://links.publifying.com/api/v1/links"
def shorten(url: str, **options) -> dict:
response = requests.post(
ENDPOINT,
json={"url": url, **options},
headers={
"Authorization": f"Bearer {{API_KEY}}",
"Accept": "application/json",
},
timeout=10,
)
response.raise_for_status()
return response.json().get("data", response.json())
link = shorten("https://example.com/very/long/url", title="August newsletter")
print(link["short_url"])
// Node 18+ has fetch built in — no dependencies required.
import { setTimeout as sleep } from "node:timers/promises";
const API_KEY = process.env.PUBLIFYING_API_KEY;
export async function shortenMany(urls) {
const results = [];
for (const url of urls) {
const response = await fetch("https://links.publifying.com/api/v1/links", {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ url }),
});
// Back off politely when you hit the plan rate limit.
if (response.status === 429) {
await sleep(Number(response.headers.get("Retry-After") ?? 1) * 1000);
continue;
}
results.push(await response.json());
}
return results;
}
Response shape
Every link is returned in this format:
{
"data": {
"id": 1842,
"code": "k7Rm2xQ",
"short_url": "https://links.publifying.com/k7Rm2xQ",
"destination": "https://example.com/a/very/long/marketing/url",
"domain": "example.com",
"title": "August newsletter",
"tags": ["newsletter", "august"],
"source": "api",
"is_active": true,
"expires_at": null,
"max_clicks": null,
"clicks": 0,
"unique_clicks": 0,
"last_clicked_at": null,
"created_at": "2026-08-01T21:29:01+00:00",
"updated_at": "2026-08-01T21:29:01+00:00"
}
}