Developers
ShrinkRL API
Shorten URLs, set custom slugs, read click analytics, and create links in bulk — over a simple JSON REST API. No API key. No auth. Just call it.
Overview
Every endpoint is open — no headers or keys required. A bulk request counts as a single call against the rate limit, no matter how many URLs it carries. High-volume partners can request a higher per-IP limit.
og_image: null and metadata: {}; fetch the link again with GET /api/v1/urls/:slug a moment later to get them.If your destination isn’t ready at create time — a cover image uploaded later, a page still in draft — the scrape captures whatever was there and the preview stays that way. You are not stuck with it: send the metadata yourself at create, or fix it afterwards with PATCH / refresh. The slug never has to change.Create a short link
/api/v1/urlsBody — wrap fields in a url object. slug (custom, 3–20 chars of a–z, A–Z, 0–9, -_) and expires_at (ISO-8601) are optional.
POST /api/v1/urls
Content-Type: application/json
{ "url": { "original_url": "https://example.com/a/very/long/page" } }{
"short_url": "https://g.shrinkrl.com/kDgxDc",
"original_url": "https://example.com/a/very/long/page",
"og_image": null,
"slug": "kDgxDc",
"metadata": {}
}Set the preview yourself (skips the scrape)
Pass any of title, description, image, site_name and we use them verbatim — the background scrape is skipped entirely, so timing stops mattering.
{ "url": {
"original_url": "https://example.com/event/forty-under-40",
"title": "FORTY UNDER 40 GHANA 2026",
"description": "Nominations close 30 September.",
"image": "https://cdn.example.com/cover.png",
"site_name": "Example Events"
} }Custom-slug collisions are idempotent
- Same
original_url→ 200 with the existing link (safe to retry — no duplicate created). - Different
original_url→ 409 with the existing link in the body pluserrors.
Read a link & its stats
/api/v1/urls/:slugReturns the link plus metadata and click count. Does not count as a click — use this for dashboards and previews.
{
"short_url": "https://g.shrinkrl.com/kDgxDc",
"original_url": "https://example.com/a/very/long/page",
"og_image": "https://.../preview.png",
"click_count": 42,
"created_at": "2026-05-29T08:29:10.409Z",
"slug": "kDgxDc",
"metadata": {
"title": "Example page title",
"description": "A short description of the destination.",
"image": "https://.../preview.png",
"site_name": "Example.com"
}
}/api/v1/urls/:slug/dataSame payload, but counts as a click. Use it when a user actually opens the link.
/api/v1/urls/:slugDeletes the link. Returns 204 No Content.
Edit a link’s preview
The card people see when a link is shared on WhatsApp, Slack, X or Facebook. Both endpoints work on links that are already circulating — the slug and destination stay exactly as they are, so nothing you have shared breaks.
/api/v1/urls/:slugSets the preview directly. Send only the fields you want to change; anything you omit is left alone. Returns 200 with the updated link.
PATCH /api/v1/urls/kDgxDc
Content-Type: application/json
{ "url": {
"image": "https://cdn.example.com/cover-uploaded-later.png",
"title": "FORTY UNDER 40 GHANA 2026"
} }original_url and slug are not editable here — sending them is ignored rather than rejected. Previews are presentation; re-pointing a link that people have already shared is not something an open, keyless API should allow. Delete and re-create if you truly need a different destination.
/api/v1/urls/:slug/refreshRe-scrapes the destination and overwrites the stored preview with whatever it finds now. Use this when the destination page itself has been fixed. Returns 202 Accepted immediately — the scrape runs in the background, so read the link back a second or two later.
{ "status": "queued", "slug": "kDgxDc" }Fields the re-scrape can’t find are left as they were, so a destination that is temporarily down won’t blank out a preview you set by hand.
Bulk operations
/api/v1/urls/bulk_create≤ 500 URLs are created immediately and returned in the response. > 500 are processed in the background and you get a bulk_id to poll.
{ "urls": [
{ "original_url": "https://a.com" },
{ "original_url": "https://b.com", "slug": "launch-b" }
] }{
"status": "completed",
"total": 2, "successful": 2, "failed": 0,
"urls": [ { "short_url": "...", "slug": "...", ... } ],
"errors": []
}{
"bulk_id": "uuid",
"status": "processing",
"check_status_url": "/api/v1/urls/bulk/uuid/status"
}/api/v1/urls/bulk/:bulk_id/statusPoll until status is completed, then read urls / errors.
/api/v1/urls/bulk_dataRead up to 1,000 links at once.
{ "slugs": ["abc123", "def456"] }Redirects & health
/r/:slugServer-side 302 redirect to the original URL (the click is tracked automatically). Sharing the short link https://g.shrinkrl.com/:slug works too.
/healthReturns { "status": "OK" } when the API is healthy.
Code examples
curl -s -X POST https://api.shrinkrl.com/api/v1/urls \
-H "Content-Type: application/json" \
-d '{"url":{"original_url":"https://example.com"}}'const API = "https://api.shrinkrl.com";
const res = await fetch(`${API}/api/v1/urls`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: { original_url: "https://example.com" } }),
});
const { short_url } = await res.json();
// metadata is async — fetch it a moment later (non-tracking endpoint):
const link = await (await fetch(`${API}/api/v1/urls/${slug}`)).json();import requests
API = "https://api.shrinkrl.com"
r = requests.post(f"{API}/api/v1/urls",
json={"url": {"original_url": "https://example.com"}})
r.raise_for_status()
short_url = r.json()["short_url"]