Skip to content
Publifying Links
REST API · v1

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